packages feed

Spock 0.8.1.0 → 0.9.0.0

raw patch · 14 files changed

+470/−236 lines, 14 filesdep +crypto-randomdep ~basedep ~reroutedep ~unordered-containersPVP ok

version bump matches the API change (PVP)

Dependencies added: crypto-random

Dependency ranges changed: base, reroute, unordered-containers, wai-extra

API changes (from Hackage documentation)

- Web.Spock.Safe: data SpockT m a
- Web.Spock.Safe: instance (Monad m, Functor m) => Applicative (SpockT m)
- Web.Spock.Safe: instance Functor m => Functor (SpockT m)
- Web.Spock.Safe: instance Monad m => Monad (SpockT m)
- Web.Spock.Safe: instance MonadIO m => MonadIO (SpockT m)
- Web.Spock.Safe: instance MonadTrans SpockT
- Web.Spock.Shared: data ActionT m a
+ Web.Spock.Safe: data SpockCtxT ctx m a
+ Web.Spock.Safe: instance (Monad m, Functor m) => Applicative (SpockCtxT ctx m)
+ Web.Spock.Safe: instance Functor m => Functor (SpockCtxT ctx m)
+ Web.Spock.Safe: instance Monad m => Monad (SpockCtxT ctx m)
+ Web.Spock.Safe: instance MonadIO m => MonadIO (SpockCtxT ctx m)
+ Web.Spock.Safe: instance MonadTrans (SpockCtxT ctx)
+ Web.Spock.Safe: prehook :: MonadIO m => ActionCtxT ctx m ctx' -> SpockCtxT ctx' m () -> SpockCtxT ctx m ()
+ Web.Spock.Safe: type SpockCtxM ctx conn sess st = SpockCtxT ctx (WebStateM conn sess st)
+ Web.Spock.Safe: type SpockT = SpockCtxT ()
+ Web.Spock.Shared: PCNoDatabase :: PoolOrConn ()
+ Web.Spock.Shared: SessionHooks :: (HashMap SessionId a -> IO ()) -> SessionHooks a
+ Web.Spock.Shared: data ActionCtxT ctx m a
+ Web.Spock.Shared: data SessionHooks a
+ Web.Spock.Shared: defaultSessionHooks :: SessionHooks a
+ Web.Spock.Shared: getContext :: MonadIO m => ActionCtxT ctx m ctx
+ Web.Spock.Shared: reqMethod :: MonadIO m => ActionCtxT ctx m StdMethod
+ Web.Spock.Shared: runInContext :: MonadIO m => ctx' -> ActionCtxT ctx' m a -> ActionCtxT ctx m a
+ Web.Spock.Shared: sc_hooks :: SessionCfg a -> SessionHooks a
+ Web.Spock.Shared: sh_removed :: SessionHooks a -> HashMap SessionId a -> IO ()
+ Web.Spock.Shared: type ActionT = ActionCtxT ()
+ Web.Spock.Shared: type SpockActionCtx ctx conn sess st = ActionCtxT ctx (WebStateM conn sess st)
- Web.Spock.Safe: delete :: MonadIO m => Path xs -> HVectElim xs (ActionT m ()) -> SpockT m ()
+ Web.Spock.Safe: delete :: (HasRep xs, MonadIO m) => Path xs -> HVectElim xs (ActionCtxT ctx m ()) -> SpockCtxT ctx m ()
- Web.Spock.Safe: get :: MonadIO m => Path xs -> HVectElim xs (ActionT m ()) -> SpockT m ()
+ Web.Spock.Safe: get :: (HasRep xs, MonadIO m) => Path xs -> HVectElim xs (ActionCtxT ctx m ()) -> SpockCtxT ctx m ()
- Web.Spock.Safe: getpost :: MonadIO m => Path xs -> HVectElim xs (ActionT m ()) -> SpockT m ()
+ Web.Spock.Safe: getpost :: (HasRep xs, MonadIO m) => Path xs -> HVectElim xs (ActionCtxT ctx m ()) -> SpockCtxT ctx m ()
- Web.Spock.Safe: head :: MonadIO m => Path xs -> HVectElim xs (ActionT m ()) -> SpockT m ()
+ Web.Spock.Safe: head :: (HasRep xs, MonadIO m) => Path xs -> HVectElim xs (ActionCtxT ctx m ()) -> SpockCtxT ctx m ()
- Web.Spock.Safe: hookAny :: Monad m => StdMethod -> ([Text] -> ActionT m ()) -> SpockT m ()
+ Web.Spock.Safe: hookAny :: Monad m => StdMethod -> ([Text] -> ActionCtxT ctx m ()) -> SpockCtxT ctx m ()
- Web.Spock.Safe: hookRoute :: Monad m => StdMethod -> Path xs -> HVectElim xs (ActionT m ()) -> SpockT m ()
+ Web.Spock.Safe: hookRoute :: (HasRep xs, Monad m) => StdMethod -> Path xs -> HVectElim xs (ActionCtxT ctx m ()) -> SpockCtxT ctx m ()
- Web.Spock.Safe: middleware :: Monad m => Middleware -> SpockT m ()
+ Web.Spock.Safe: middleware :: Monad m => Middleware -> SpockCtxT ctx m ()
- Web.Spock.Safe: patch :: MonadIO m => Path xs -> HVectElim xs (ActionT m ()) -> SpockT m ()
+ Web.Spock.Safe: patch :: (HasRep xs, MonadIO m) => Path xs -> HVectElim xs (ActionCtxT ctx m ()) -> SpockCtxT ctx m ()
- Web.Spock.Safe: post :: MonadIO m => Path xs -> HVectElim xs (ActionT m ()) -> SpockT m ()
+ Web.Spock.Safe: post :: (HasRep xs, MonadIO m) => Path xs -> HVectElim xs (ActionCtxT ctx m ()) -> SpockCtxT ctx m ()
- Web.Spock.Safe: put :: MonadIO m => Path xs -> HVectElim xs (ActionT m ()) -> SpockT m ()
+ Web.Spock.Safe: put :: (HasRep xs, MonadIO m) => Path xs -> HVectElim xs (ActionCtxT ctx m ()) -> SpockCtxT ctx m ()
- Web.Spock.Safe: subcomponent :: Monad m => Path [] -> SpockT m () -> SpockT m ()
+ Web.Spock.Safe: subcomponent :: Monad m => Path [] -> SpockCtxT ctx m () -> SpockCtxT ctx m ()
- Web.Spock.Safe: type SpockM conn sess st a = SpockT (WebStateM conn sess st) a
+ Web.Spock.Safe: type SpockM conn sess st = SpockCtxM () conn sess st
- Web.Spock.Shared: PCConn :: (ConnBuilder a) -> PoolOrConn a
+ Web.Spock.Shared: PCConn :: ConnBuilder a -> PoolOrConn a
- Web.Spock.Shared: PCPool :: (Pool a) -> PoolOrConn a
+ Web.Spock.Shared: PCPool :: Pool a -> PoolOrConn a
- Web.Spock.Shared: SessionCfg :: Text -> NominalDiffTime -> Int -> Bool -> a -> Maybe (SessionPersistCfg a) -> NominalDiffTime -> SessionCfg a
+ Web.Spock.Shared: SessionCfg :: Text -> NominalDiffTime -> Int -> Bool -> a -> Maybe (SessionPersistCfg a) -> NominalDiffTime -> SessionHooks a -> SessionCfg a
- Web.Spock.Shared: body :: MonadIO m => ActionT m ByteString
+ Web.Spock.Shared: body :: MonadIO m => ActionCtxT ctx m ByteString
- Web.Spock.Shared: bytes :: MonadIO m => ByteString -> ActionT m a
+ Web.Spock.Shared: bytes :: MonadIO m => ByteString -> ActionCtxT ctx m a
- Web.Spock.Shared: clearAllSessions :: SpockAction conn sess st ()
+ Web.Spock.Shared: clearAllSessions :: SpockActionCtx ctx conn sess st ()
- Web.Spock.Shared: cookie :: MonadIO m => Text -> ActionT m (Maybe Text)
+ Web.Spock.Shared: cookie :: MonadIO m => Text -> ActionCtxT ctx m (Maybe Text)
- Web.Spock.Shared: deleteCookie :: MonadIO m => Text -> ActionT m ()
+ Web.Spock.Shared: deleteCookie :: MonadIO m => Text -> ActionCtxT ctx m ()
- Web.Spock.Shared: file :: MonadIO m => Text -> FilePath -> ActionT m a
+ Web.Spock.Shared: file :: MonadIO m => Text -> FilePath -> ActionCtxT ctx m a
- Web.Spock.Shared: files :: MonadIO m => ActionT m (HashMap Text UploadedFile)
+ Web.Spock.Shared: files :: MonadIO m => ActionCtxT ctx m (HashMap Text UploadedFile)
- Web.Spock.Shared: getSessionId :: SpockAction conn sess st SessionId
+ Web.Spock.Shared: getSessionId :: SpockActionCtx ctx conn sess st SessionId
- Web.Spock.Shared: header :: MonadIO m => Text -> ActionT m (Maybe Text)
+ Web.Spock.Shared: header :: MonadIO m => Text -> ActionCtxT ctx m (Maybe Text)
- Web.Spock.Shared: html :: MonadIO m => Text -> ActionT m a
+ Web.Spock.Shared: html :: MonadIO m => Text -> ActionCtxT ctx m a
- Web.Spock.Shared: json :: (ToJSON a, MonadIO m) => a -> ActionT m b
+ Web.Spock.Shared: json :: (ToJSON a, MonadIO m) => a -> ActionCtxT ctx m b
- Web.Spock.Shared: jsonBody :: (MonadIO m, FromJSON a) => ActionT m (Maybe a)
+ Web.Spock.Shared: jsonBody :: (MonadIO m, FromJSON a) => ActionCtxT ctx m (Maybe a)
- Web.Spock.Shared: jsonBody' :: (MonadIO m, FromJSON a) => ActionT m a
+ Web.Spock.Shared: jsonBody' :: (MonadIO m, FromJSON a) => ActionCtxT ctx m a
- Web.Spock.Shared: jumpNext :: MonadIO m => ActionT m a
+ Web.Spock.Shared: jumpNext :: MonadIO m => ActionCtxT ctx m a
- Web.Spock.Shared: lazyBytes :: MonadIO m => ByteString -> ActionT m a
+ Web.Spock.Shared: lazyBytes :: MonadIO m => ByteString -> ActionCtxT ctx m a
- Web.Spock.Shared: mapAllSessions :: (sess -> STM sess) -> SpockAction conn sess st ()
+ Web.Spock.Shared: mapAllSessions :: (sess -> STM sess) -> SpockActionCtx ctx conn sess st ()
- Web.Spock.Shared: middlewarePass :: MonadIO m => ActionT m a
+ Web.Spock.Shared: middlewarePass :: MonadIO m => ActionCtxT ctx m a
- Web.Spock.Shared: modifyReadSession :: (sess -> sess) -> SpockAction conn sess st sess
+ Web.Spock.Shared: modifyReadSession :: (sess -> sess) -> SpockActionCtx ctx conn sess st sess
- Web.Spock.Shared: modifySession :: (sess -> sess) -> SpockAction conn sess st ()
+ Web.Spock.Shared: modifySession :: (sess -> sess) -> SpockActionCtx ctx conn sess st ()
- Web.Spock.Shared: modifySession' :: (sess -> (sess, a)) -> SpockAction conn sess st a
+ Web.Spock.Shared: modifySession' :: (sess -> (sess, a)) -> SpockActionCtx ctx conn sess st a
- Web.Spock.Shared: modifyVault :: MonadIO m => (Vault -> Vault) -> ActionT m ()
+ Web.Spock.Shared: modifyVault :: MonadIO m => (Vault -> Vault) -> ActionCtxT ctx m ()
- Web.Spock.Shared: param :: (PathPiece p, MonadIO m) => Text -> ActionT m (Maybe p)
+ Web.Spock.Shared: param :: (PathPiece p, MonadIO m) => Text -> ActionCtxT ctx m (Maybe p)
- Web.Spock.Shared: param' :: (PathPiece p, MonadIO m) => Text -> ActionT m p
+ Web.Spock.Shared: param' :: (PathPiece p, MonadIO m) => Text -> ActionCtxT ctx m p
- Web.Spock.Shared: params :: MonadIO m => ActionT m [(Text, Text)]
+ Web.Spock.Shared: params :: MonadIO m => ActionCtxT ctx m [(Text, Text)]
- Web.Spock.Shared: preferredFormat :: MonadIO m => ActionT m ClientPreferredFormat
+ Web.Spock.Shared: preferredFormat :: MonadIO m => ActionCtxT ctx m ClientPreferredFormat
- Web.Spock.Shared: queryVault :: MonadIO m => Key a -> ActionT m (Maybe a)
+ Web.Spock.Shared: queryVault :: MonadIO m => Key a -> ActionCtxT ctx m (Maybe a)
- Web.Spock.Shared: rawHeader :: MonadIO m => HeaderName -> ActionT m (Maybe ByteString)
+ Web.Spock.Shared: rawHeader :: MonadIO m => HeaderName -> ActionCtxT ctx m (Maybe ByteString)
- Web.Spock.Shared: readSession :: SpockAction conn sess st sess
+ Web.Spock.Shared: readSession :: SpockActionCtx ctx conn sess st sess
- Web.Spock.Shared: redirect :: MonadIO m => Text -> ActionT m a
+ Web.Spock.Shared: redirect :: MonadIO m => Text -> ActionCtxT ctx m a
- Web.Spock.Shared: request :: MonadIO m => ActionT m Request
+ Web.Spock.Shared: request :: MonadIO m => ActionCtxT ctx m Request
- Web.Spock.Shared: requireBasicAuth :: MonadIO m => Text -> (Text -> Text -> m Bool) -> ActionT m a -> ActionT m a
+ Web.Spock.Shared: requireBasicAuth :: MonadIO m => Text -> (Text -> Text -> m Bool) -> ActionCtxT ctx m a -> ActionCtxT ctx m a
- Web.Spock.Shared: response :: MonadIO m => (Status -> ResponseHeaders -> Response) -> ActionT m a
+ Web.Spock.Shared: response :: MonadIO m => (Status -> ResponseHeaders -> Response) -> ActionCtxT ctx m a
- Web.Spock.Shared: setCookie :: MonadIO m => Text -> Text -> NominalDiffTime -> ActionT m ()
+ Web.Spock.Shared: setCookie :: MonadIO m => Text -> Text -> NominalDiffTime -> ActionCtxT ctx m ()
- Web.Spock.Shared: setCookie' :: MonadIO m => Text -> Text -> UTCTime -> ActionT m ()
+ Web.Spock.Shared: setCookie' :: MonadIO m => Text -> Text -> UTCTime -> ActionCtxT ctx m ()
- Web.Spock.Shared: setHeader :: MonadIO m => Text -> Text -> ActionT m ()
+ Web.Spock.Shared: setHeader :: MonadIO m => Text -> Text -> ActionCtxT ctx m ()
- Web.Spock.Shared: setStatus :: MonadIO m => Status -> ActionT m ()
+ Web.Spock.Shared: setStatus :: MonadIO m => Status -> ActionCtxT ctx m ()
- Web.Spock.Shared: stream :: MonadIO m => StreamingBody -> ActionT m a
+ Web.Spock.Shared: stream :: MonadIO m => StreamingBody -> ActionCtxT ctx m a
- Web.Spock.Shared: text :: MonadIO m => Text -> ActionT m a
+ Web.Spock.Shared: text :: MonadIO m => Text -> ActionCtxT ctx m a
- Web.Spock.Shared: type SpockAction conn sess st = ActionT (WebStateM conn sess st)
+ Web.Spock.Shared: type SpockAction conn sess st = SpockActionCtx () conn sess st
- Web.Spock.Shared: writeSession :: sess -> SpockAction conn sess st ()
+ Web.Spock.Shared: writeSession :: sess -> SpockActionCtx ctx conn sess st ()

Files

README.md view
@@ -2,33 +2,17 @@ =====  [![Build Status](https://travis-ci.org/agrafix/Spock.svg)](https://travis-ci.org/agrafix/Spock)--[![Hackage Deps](https://img.shields.io/hackage-deps/v/Spock.svg)](http://packdeps.haskellers.com/reverse/Spock)--For more information and tutorials, visit [spock.li](http://www.spock.li)+[![Hackage](https://img.shields.io/hackage/v/Spock.svg)](http://hackage.haskell.org/package/Spock)  ## Intro  Hackage: [Spock](http://hackage.haskell.org/package/Spock)-Tutorial: [Spock Tutorial](https://www.spock.li/tutorial/)+Stackage: [Spock](https://www.stackage.org/package/Spock) -Another Haskell web framework for rapid development: This toolbox provides-everything you need to get a quick start into web hacking with haskell:+Another Haskell web framework for rapid development -* fast routing (both typesafe and "untyped")-* middleware-* json-* sessions-* cookies-* database helper-* csrf-protection -Benchmarks:--* https://github.com/philopon/apiary-benchmark-* https://github.com/agrafix/Spock-scotty-benchmark--## Usage (Typesafe, recommended)+## Library Usage Example  ```haskell {-# LANGUAGE OverloadedStrings #-}@@ -40,33 +24,47 @@     runSpock 3000 $ spockT id $     do get ("echo" <//> var) $ \something ->         text $ T.concat ["Echo: ", something]+ ``` -(read more at [Type-safe routing in Spock](https://www.spock.li/2015/04/19/type-safe_routing.html))+For more examples check the examples/ directory. -## Usage (Simple)+## Install -```haskell-{-# LANGUAGE OverloadedStrings #-}-import Web.Spock.Simple+* Using cabal: `cabal install Spock`+* Using Stack: `stack install Spock`+* From Source (cabal): `git clone https://github.com/agrafix/Spock.git && cd Spock && cabal install`+* From Source (stack): `git clone https://github.com/agrafix/Spock.git && cd Spock && stack build` -import qualified Data.Text as T+## Features -main =-    runSpock 3000 $ spockT id $-    do get ("echo" <//> ":something") $-        do Just something <- param "something"-           text $ T.concat ["Echo: ", something]-       get ("regex" <//> "{number:^[0-9]+$}") $-        do Just number <- param "number"-           text $ T.concat ["Just a number: ", number]-```+Another Haskell web framework for rapid development: This toolbox provides+everything you need to get a quick start into web hacking with haskell: -## Install+* fast routing (both typesafe and "untyped")+* middleware+* json+* sessions+* cookies+* database helper+* csrf-protection+* typesafe contexts -* Using cabal: `cabal install Spock`-* From Source: `git clone https://github.com/agrafix/Spock.git && cd Spock && cabal install`+Benchmarks: +* https://github.com/philopon/apiary-benchmark+* https://github.com/agrafix/Spock-scotty-benchmark++## Important Links++* [Tutorial](https://www.spock.li/tutorial/)+* [Type-safe routing in Spock](https://www.spock.li/2015/04/19/type-safe_routing.html) ++### Talks++* German: [Moderne typsichere Web-Entwicklung mit Haskell](https://dl.dropboxusercontent.com/u/15078797/talks/typesafe-webdev-2015.pdf) (by Alexander Thiemann)+* German: [reroute-talk](https://github.com/timjb/reroute-talk) (by Tim Baumann)+ ## Candy  ### Extensions@@ -89,16 +87,11 @@ * [makeci](https://github.com/openbrainsrc/makeci) * [curry-recipes](https://github.com/timjb/reroute-talk/tree/06574561918b50c1809f1e24ec7faeff731fddcf/curry-recipes) -## Talks--* German: [Moderne typsichere Web-Entwicklung mit Haskell](https://dl.dropboxusercontent.com/u/15078797/talks/typesafe-webdev-2015.pdf) (by Alexander Thiemann)-* German: [reroute-talk](https://github.com/timjb/reroute-talk) (by Tim Baumann)- ## Companies / Projects using Spock -* http://cp-med.com/-* http://openbrain.co.uk/-* http://findmelike.com/+* http://cp-med.com+* http://openbrain.co.uk+* http://findmelike.com * https://www.tramcloud.net * http://thitp.de @@ -114,3 +107,17 @@  * Tim Baumann [Github](https://github.com/timjb) (lot's of help with typesafe routing) * Tom Nielsen [Github](https://github.com/glutamate)  (much feedback and small improvements)+++## Misc++### Supported GHC Versions++* 7.6.3+* 7.8.4+* 7.10.2++### License++Released under the BSD3 license.+(c) 2013 - 2015 Alexander Thiemann
Spock.cabal view
@@ -1,5 +1,5 @@ name:                Spock-version:             0.8.1.0+version:             0.9.0.0 synopsis:            Another Haskell web framework for rapid development description:         This toolbox provides everything you need to get a quick start into web hacking with haskell:                      .@@ -56,6 +56,7 @@                        bytestring >=0.10,                        case-insensitive >=1.1,                        containers >=0.5,+                       crypto-random >= 0.0.8,                        directory >=1.2,                        focus >=0.1,                        hashable >=1.2,@@ -67,7 +68,7 @@                        old-locale >=1.0,                        path-pieces >=0.1.4,                        random >=1.0,-                       reroute >=0.3,+                       reroute >=0.3.1,                        resource-pool >=0.2,                        resourcet >= 0.4,                        stm >=2.4,@@ -103,19 +104,12 @@                        stm,                        reroute,                        text,-                       wai+                       unordered-containers,+                       wai,+                       wai-extra    ghc-options: -Wall -fno-warn-orphans -benchmark spock-simple-example-  type:                exitcode-stdio-1.0-  ghc-options:         -auto-all -Wall -O2-  hs-source-dirs:      examples/simple-  main-is:             Main.hs-  build-depends:-    base,-    Spock- source-repository head   type:     git-  location: git://github.com/agrafix/Spock.git+  location: https://github.com/agrafix/Spock
− examples/simple/Main.hs
@@ -1,12 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-module Main where--import Web.Spock.Safe--main :: IO ()-main =-    runSpock 8080 $ spockT id $-    do get "foo" $-           text "bar"-       get ("hello" <//> var) $ \name ->-           text name
src/Web/Spock/Internal/Core.hs view
@@ -1,7 +1,8 @@ {-# LANGUAGE CPP #-}+{-# LANGUAGE DoAndIfThenElse #-}+{-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-}-{-# LANGUAGE DoAndIfThenElse #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeFamilies #-} module Web.Spock.Internal.Core@@ -47,6 +48,8 @@     do sessionMgr <- createSessionManager sessionCfg        connectionPool <-            case poolOrConn of+             PCNoDatabase ->+                 createPool (return ()) (const $ return ()) 5 60 5              PCPool p ->                  return p              PCConn cb ->
src/Web/Spock/Internal/CoreAction.hs view
@@ -6,11 +6,13 @@     ( ActionT     , UploadedFile (..)     , request, header, rawHeader, cookie, body, jsonBody, jsonBody'+    , reqMethod     , files, params, param, param', setStatus, setHeader, redirect     , jumpNext, middlewarePass, modifyVault, queryVault     , setCookie, setCookie', deleteCookie     , bytes, lazyBytes, text, html, file, json, stream, response     , requireBasicAuth+    , getContext, runInContext     , preferredFormat, ClientPreferredFormat(..)     ) where@@ -26,10 +28,13 @@ import Control.Monad.Error #endif import Control.Monad.Reader+import Control.Monad.RWS.Strict (runRWST) import Control.Monad.State hiding (get, put)+import qualified Control.Monad.State as ST import Data.Monoid import Data.Time import Network.HTTP.Types.Header (HeaderName, ResponseHeaders)+import Network.HTTP.Types.Method import Network.HTTP.Types.Status import Prelude hiding (head) #if MIN_VERSION_time(1,5,0)@@ -50,24 +55,24 @@ import qualified Network.Wai as Wai  -- | Get the original Wai Request object-request :: MonadIO m => ActionT m Wai.Request+request :: MonadIO m => ActionCtxT ctx m Wai.Request request = asks ri_request {-# INLINE request #-}  -- | Read a header-header :: MonadIO m => T.Text -> ActionT m (Maybe T.Text)+header :: MonadIO m => T.Text -> ActionCtxT ctx m (Maybe T.Text) header t =     liftM (fmap T.decodeUtf8) $ rawHeader (CI.mk (T.encodeUtf8 t)) {-# INLINE header #-}  -- | Read a header without converting it to text-rawHeader :: MonadIO m => HeaderName -> ActionT m (Maybe BS.ByteString)+rawHeader :: MonadIO m => HeaderName -> ActionCtxT ctx m (Maybe BS.ByteString) rawHeader t =     liftM (lookup t . Wai.requestHeaders) request {-# INLINE rawHeader #-}  -- | Read a cookie-cookie :: MonadIO m => T.Text -> ActionT m (Maybe T.Text)+cookie :: MonadIO m => T.Text -> ActionCtxT ctx m (Maybe T.Text) cookie name =     do req <- request        return $ lookup "cookie" (Wai.requestHeaders req) >>= lookup name . parseCookies . T.decodeUtf8@@ -78,7 +83,7 @@ {-# INLINE cookie #-}  -- | Tries to dected the preferred format of the response using the Accept header-preferredFormat :: MonadIO m => ActionT m ClientPreferredFormat+preferredFormat :: MonadIO m => ActionCtxT ctx m ClientPreferredFormat preferredFormat =   do mAccept <- header "accept"      case mAccept of@@ -87,8 +92,13 @@          return $ detectPreferredFormat t {-# INLINE preferredFormat #-} +-- | Returns the current request method, e.g. 'GET'+reqMethod :: MonadIO m => ActionCtxT ctx m StdMethod+reqMethod = asks ri_method+{-# INLINE reqMethod #-}+ -- | Get the raw request body-body :: MonadIO m => ActionT m BS.ByteString+body :: MonadIO m => ActionCtxT ctx m BS.ByteString body =     do req <- request        let parseBody = liftIO $ Wai.requestBody req@@ -101,14 +111,14 @@ {-# INLINE body #-}  -- | Parse the request body as json-jsonBody :: (MonadIO m, A.FromJSON a) => ActionT m (Maybe a)+jsonBody :: (MonadIO m, A.FromJSON a) => ActionCtxT ctx m (Maybe a) jsonBody =     do b <- body        return $ A.decodeStrict b {-# INLINE jsonBody #-}  -- | Parse the request body as json and fails with 500 status code on error-jsonBody' :: (MonadIO m, A.FromJSON a) => ActionT m a+jsonBody' :: (MonadIO m, A.FromJSON a) => ActionCtxT ctx m a jsonBody' =     do b <- body        case A.eitherDecodeStrict' b of@@ -120,21 +130,21 @@ {-# INLINE jsonBody' #-}  -- | Get uploaded files-files :: MonadIO m => ActionT m (HM.HashMap T.Text UploadedFile)+files :: MonadIO m => ActionCtxT ctx m (HM.HashMap T.Text UploadedFile) files =     asks ri_files {-# INLINE files #-}  -- | Get all request params-params :: MonadIO m => ActionT m [(T.Text, T.Text)]+params :: MonadIO m => ActionCtxT ctx m [(T.Text, T.Text)] params =     do p <- asks ri_params        qp <- asks ri_queryParams        return (qp ++ map (first unCaptureVar) (HM.toList p)) {-# INLINE params #-} --- | Read a request param. Spock looks in route captures first, then in POST variables and at last in GET variables-param :: (PathPiece p, MonadIO m) => T.Text -> ActionT m (Maybe p)+-- | Read a request param. Spock looks in route captures first (in simple routing), then in POST variables and at last in GET variables+param :: (PathPiece p, MonadIO m) => T.Text -> ActionCtxT ctx m (Maybe p) param k =     do p <- asks ri_params        qp <- asks ri_queryParams@@ -151,7 +161,7 @@ {-# INLINE param #-}  -- | Like 'param', but outputs an error when a param is missing-param' :: (PathPiece p, MonadIO m) => T.Text -> ActionT m p+param' :: (PathPiece p, MonadIO m) => T.Text -> ActionCtxT ctx m p param' k =     do mParam <- param k        case mParam of@@ -163,7 +173,7 @@ {-# INLINE param' #-}  -- | Set a response status-setStatus :: MonadIO m => Status -> ActionT m ()+setStatus :: MonadIO m => Status -> ActionCtxT ctx m () setStatus s =     modify $ \rs -> rs { rs_status = s } {-# INLINE setStatus #-}@@ -172,7 +182,7 @@ -- is allowed to occur multiple times (as in RFC 2616), it will -- be appended. Otherwise the previous value is overwritten. -- See 'setMultiHeader'.-setHeader :: MonadIO m => T.Text -> T.Text -> ActionT m ()+setHeader :: MonadIO m => T.Text -> T.Text -> ActionCtxT ctx m () setHeader k v =     do let ciVal = CI.mk $ T.encodeUtf8 k        case HM.lookup ciVal multiHeaderMap of@@ -183,7 +193,7 @@ {-# INLINE setHeader #-}  -- | INTERNAL: Set a response header that can occur multiple times. (eg: Cache-Control)-setMultiHeader :: MonadIO m => MultiHeader -> T.Text -> ActionT m ()+setMultiHeader :: MonadIO m => MultiHeader -> T.Text -> ActionCtxT ctx m () setMultiHeader k v =     modify $ \rs ->         rs@@ -193,7 +203,7 @@ {-# INLINE setMultiHeader #-}  -- | INTERNAL: Unsafely set a header (no checking if the header can occur multiple times)-setHeaderUnsafe :: MonadIO m => T.Text -> T.Text -> ActionT m ()+setHeaderUnsafe :: MonadIO m => T.Text -> T.Text -> ActionCtxT ctx m () setHeaderUnsafe k v =     modify $ \rs ->         rs@@ -204,12 +214,12 @@   -- | Abort the current action and jump the next one matching the route-jumpNext :: MonadIO m => ActionT m a+jumpNext :: MonadIO m => ActionCtxT ctx m a jumpNext = throwError ActionTryNext {-# INLINE jumpNext #-}  -- | Redirect to a given url-redirect :: MonadIO m => T.Text -> ActionT m a+redirect :: MonadIO m => T.Text -> ActionCtxT ctx m a redirect = throwError . ActionRedirect {-# INLINE redirect #-} @@ -217,39 +227,39 @@ -- this to pass request handling to the underlying application. -- If Spock is not uses as a middleware, or there is no underlying application -- this will result in 404 error.-middlewarePass :: MonadIO m => ActionT m a+middlewarePass :: MonadIO m => ActionCtxT ctx m a middlewarePass = throwError ActionMiddlewarePass {-# INLINE middlewarePass #-}  -- | Modify the vault (useful for sharing data between middleware and app)-modifyVault :: MonadIO m => (V.Vault -> V.Vault) -> ActionT m ()+modifyVault :: MonadIO m => (V.Vault -> V.Vault) -> ActionCtxT ctx m () modifyVault f =     do vaultIf <- asks ri_vaultIf        liftIO $ vi_modifyVault vaultIf f {-# INLINE modifyVault #-}  -- | Query the vault-queryVault :: MonadIO m => V.Key a -> ActionT m (Maybe a)+queryVault :: MonadIO m => V.Key a -> ActionCtxT ctx m (Maybe a) queryVault k =     do vaultIf <- asks ri_vaultIf        liftIO $ vi_lookupKey vaultIf k {-# INLINE queryVault #-}  -- | Set a cookie living for a given number of seconds-setCookie :: MonadIO m => T.Text -> T.Text -> NominalDiffTime -> ActionT m ()+setCookie :: MonadIO m => T.Text -> T.Text -> NominalDiffTime -> ActionCtxT ctx m () setCookie name value validSeconds =     do now <- liftIO getCurrentTime        setCookie' name value (validSeconds `addUTCTime` now) {-# INLINE setCookie #-} -deleteCookie :: MonadIO m => T.Text -> ActionT m ()+deleteCookie :: MonadIO m => T.Text -> ActionCtxT ctx m () deleteCookie name = setCookie' name T.empty epoch   where     epoch = UTCTime (fromGregorian 1970 1 1) (secondsToDiffTime 0) {-# INLINE deleteCookie #-}  -- | Set a cookie living until a specific 'UTCTime'-setCookie' :: MonadIO m => T.Text -> T.Text -> UTCTime -> ActionT m ()+setCookie' :: MonadIO m => T.Text -> T.Text -> UTCTime -> ActionCtxT ctx m () setCookie' name value validUntil =     setMultiHeader MultiHeaderSetCookie rendered     where@@ -266,54 +276,54 @@ {-# INLINE setCookie' #-}  -- | Use a custom 'Wai.Response' generator as response body.-response :: MonadIO m => (Status -> ResponseHeaders -> Wai.Response) -> ActionT m a+response :: MonadIO m => (Status -> ResponseHeaders -> Wai.Response) -> ActionCtxT ctx m a response val =     do modify $ \rs -> rs { rs_responseBody = ResponseBody val }        throwError ActionDone {-# INLINE response #-}  -- | Send a 'ByteString' as response body. Provide your own "Content-Type"-bytes :: MonadIO m => BS.ByteString -> ActionT m a+bytes :: MonadIO m => BS.ByteString -> ActionCtxT ctx m a bytes val =     lazyBytes $ BSL.fromStrict val {-# INLINE bytes #-}  -- | Send a lazy 'ByteString' as response body. Provide your own "Content-Type"-lazyBytes :: MonadIO m => BSL.ByteString -> ActionT m a+lazyBytes :: MonadIO m => BSL.ByteString -> ActionCtxT ctx m a lazyBytes val =     response $ \status headers -> Wai.responseLBS status headers val {-# INLINE lazyBytes #-}  -- | Send text as a response body. Content-Type will be "text/plain"-text :: MonadIO m => T.Text -> ActionT m a+text :: MonadIO m => T.Text -> ActionCtxT ctx m a text val =     do setHeaderUnsafe "Content-Type" "text/plain; charset=utf-8"        bytes $ T.encodeUtf8 val {-# INLINE text #-}  -- | Send a text as response body. Content-Type will be "text/html"-html :: MonadIO m => T.Text -> ActionT m a+html :: MonadIO m => T.Text -> ActionCtxT ctx m a html val =     do setHeaderUnsafe "Content-Type" "text/html; charset=utf-8"        bytes $ T.encodeUtf8 val {-# INLINE html #-}  -- | Send a file as response-file :: MonadIO m => T.Text -> FilePath -> ActionT m a+file :: MonadIO m => T.Text -> FilePath -> ActionCtxT ctx m a file contentType filePath =      do setHeaderUnsafe "Content-Type" contentType         response $ \status headers -> Wai.responseFile status headers filePath Nothing {-# INLINE file #-}  -- | Send json as response. Content-Type will be "application/json"-json :: (A.ToJSON a, MonadIO m) => a -> ActionT m b+json :: (A.ToJSON a, MonadIO m) => a -> ActionCtxT ctx m b json val =     do setHeaderUnsafe "Content-Type" "application/json; charset=utf-8"        lazyBytes $ A.encode val {-# INLINE json #-}  -- | Use a 'Wai.StreamingBody' to generate a response.-stream :: MonadIO m => Wai.StreamingBody -> ActionT m a+stream :: MonadIO m => Wai.StreamingBody -> ActionCtxT ctx m a stream val =     response $ \status headers -> Wai.responseStream status headers val {-# INLINE stream #-}@@ -326,7 +336,7 @@ -- >   requireBasicAuth "Secret Page" (\user pass -> return (user == "admin" && pass == "1234")) $ -- >   do html "This is top secret content. Login using that secret code I provided ;-)" ---requireBasicAuth :: MonadIO m => T.Text -> (T.Text -> T.Text -> m Bool) -> ActionT m a -> ActionT m a+requireBasicAuth :: MonadIO m => T.Text -> (T.Text -> T.Text -> m Bool) -> ActionCtxT ctx m a -> ActionCtxT ctx m a requireBasicAuth realmTitle authFun cont =     do mAuthHeader <- header "Authorization"        case mAuthHeader of@@ -347,3 +357,27 @@           do setStatus status401              setMultiHeader MultiHeaderWWWAuth ("Basic realm=\"" <> realmTitle <> "\"")              html "<h1>Authentication required.</h1>"++-- | Get the context of the current request+getContext :: MonadIO m => ActionCtxT ctx m ctx+getContext = asks ri_context+{-# INLINE getContext #-}++-- | Run an Action in a different context+runInContext :: MonadIO m => ctx' -> ActionCtxT ctx' m a -> ActionCtxT ctx m a+runInContext newCtx action =+    do currentEnv <- ask+       currentRespState <- ST.get+       (r, newRespState, _) <-+          lift $+          do let env =+                     currentEnv+                     { ri_context = newCtx+                     }+             runRWST (runErrorT $ runActionCtxT action) env currentRespState+       ST.put newRespState+       case r of+         Left interupt ->+             throwError interupt+         Right d -> return d+{-# INLINE runInContext #-}
src/Web/Spock/Internal/Monad.hs view
@@ -1,3 +1,4 @@+{-# OPTIONS_GHC -fno-warn-orphans #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-}
src/Web/Spock/Internal/SessionManager.hs view
@@ -12,6 +12,10 @@ import qualified Web.Spock.Internal.SessionVault as SV  import Control.Arrow (first)+#if MIN_VERSION_base(4,8,0)+#else+import Control.Applicative+#endif import Control.Concurrent import Control.Concurrent.STM import Control.Exception@@ -22,9 +26,8 @@ #else import System.Locale (defaultTimeLocale) #endif-import System.Random-import qualified Data.ByteString.Base64.URL as B64-import qualified Data.ByteString.Char8 as BSC+import qualified Crypto.Random as CR+import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Lazy as BSL import qualified Data.HashMap.Strict as HM import qualified Data.Text as T@@ -40,7 +43,8 @@  createSessionManager :: SessionCfg sess -> IO (SessionManager conn sess st) createSessionManager cfg =-    do oldSess <- loadSessions+    do pool <- CR.createEntropyPool+       oldSess <- loadSessions        cacheHM <-            atomically $            do mapV <- SV.newSessionVault@@ -56,8 +60,8 @@           , sm_modifySession = modifySessionImpl vaultKey cacheHM           , sm_clearAllSessions = clearAllSessionsImpl cacheHM           , sm_mapSessions = mapAllSessionsImpl cacheHM-          , sm_middleware = sessionMiddleware cfg vaultKey cacheHM-          , sm_addSafeAction = addSafeActionImpl vaultKey cacheHM+          , sm_middleware = sessionMiddleware pool cfg vaultKey cacheHM+          , sm_addSafeAction = addSafeActionImpl pool vaultKey cacheHM           , sm_lookupSafeAction = lookupSafeActionImpl vaultKey cacheHM           , sm_removeSafeAction = removeSafeActionImpl vaultKey cacheHM           , sm_closeSessionManager = killThread housekeepThread@@ -86,7 +90,7 @@  getSessionIdImpl :: V.Key SessionId                  -> SV.SessionVault (Session conn sess st)-                 -> SpockAction conn sess st SessionId+                 -> SpockActionCtx ctx conn sess st SessionId getSessionIdImpl vK sessionRef =     do sess <- readSessionBase vK sessionRef        return $ sess_id sess@@ -94,7 +98,7 @@ modifySessionBase :: V.Key SessionId                   -> SV.SessionVault (Session conn sess st)                   -> (Session conn sess st -> (Session conn sess st, a))-                  -> SpockAction conn sess st a+                  -> SpockActionCtx ctx conn sess st a modifySessionBase vK sessionRef modFun =     do req <- request        case V.lookup vK (Wai.vault req) of@@ -113,7 +117,7 @@  readSessionBase :: V.Key SessionId                 -> SV.SessionVault (Session conn sess st)-                -> SpockAction conn sess st (Session conn sess st)+                -> SpockActionCtx ctx conn sess st (Session conn sess st) readSessionBase vK sessionRef =     do req <- request        case V.lookup vK (Wai.vault req) of@@ -127,17 +131,19 @@                   Just session ->                       return session -addSafeActionImpl :: V.Key SessionId-                  -> SV.SessionVault (Session conn sess st)-                  -> PackedSafeAction conn sess st-                  -> SpockAction conn sess st SafeActionHash-addSafeActionImpl vaultKey sessionMapVar safeAction =+addSafeActionImpl ::+    CR.EntropyPool+    -> V.Key SessionId+    -> SV.SessionVault (Session conn sess st)+    -> PackedSafeAction conn sess st+    -> SpockActionCtx ctx conn sess st SafeActionHash+addSafeActionImpl pool vaultKey sessionMapVar safeAction =     do base <- readSessionBase vaultKey sessionMapVar        case HM.lookup safeAction (sas_reverse (sess_safeActions base)) of          Just safeActionHash ->              return safeActionHash          Nothing ->-             do safeActionHash <- liftIO (randomHash 40)+             do safeActionHash <- liftIO (randomHash pool 40)                 let f sas =                         sas                         { sas_forward = HM.insert safeActionHash safeAction (sas_forward sas)@@ -149,7 +155,7 @@ lookupSafeActionImpl :: V.Key SessionId                      -> SV.SessionVault (Session conn sess st)                      -> SafeActionHash-                     -> SpockAction conn sess st (Maybe (PackedSafeAction conn sess st))+                     -> SpockActionCtx ctx conn sess st (Maybe (PackedSafeAction conn sess st)) lookupSafeActionImpl vaultKey sessionMapVar hash =     do base <- readSessionBase vaultKey sessionMapVar        return $ HM.lookup hash (sas_forward (sess_safeActions base))@@ -157,7 +163,7 @@ removeSafeActionImpl :: V.Key SessionId                      -> SV.SessionVault (Session conn sess st)                      -> PackedSafeAction conn sess st-                     -> SpockAction conn sess st ()+                     -> SpockActionCtx ctx conn sess st () removeSafeActionImpl vaultKey sessionMapVar action =     modifySessionBase vaultKey sessionMapVar (\s -> (s { sess_safeActions = f (sess_safeActions s ) }, ()))     where@@ -172,7 +178,7 @@  readSessionImpl :: V.Key SessionId                 -> SV.SessionVault (Session conn sess st)-                -> SpockAction conn sess st sess+                -> SpockActionCtx ctx conn sess st sess readSessionImpl vK sessionRef =     do base <- readSessionBase vK sessionRef        return (sess_data base)@@ -180,25 +186,27 @@ writeSessionImpl :: V.Key SessionId                  -> SV.SessionVault (Session conn sess st)                  -> sess-                 -> SpockAction conn sess st ()+                 -> SpockActionCtx ctx conn sess st () writeSessionImpl vK sessionRef value =     modifySessionImpl vK sessionRef (const (value, ()))  modifySessionImpl :: V.Key SessionId                   -> SV.SessionVault (Session conn sess st)                   -> (sess -> (sess, a))-                  -> SpockAction conn sess st a+                  -> SpockActionCtx ctx conn sess st a modifySessionImpl vK sessionRef f =     do let modFun session =                let (sessData', out) = f (sess_data session)                in (session { sess_data = sessData' }, out)        modifySessionBase vK sessionRef modFun -sessionMiddleware :: SessionCfg sess-                  -> V.Key SessionId-                  -> SV.SessionVault (Session conn sess st)-                  -> Wai.Middleware-sessionMiddleware cfg vK sessionRef app req respond =+sessionMiddleware ::+    CR.EntropyPool+    -> SessionCfg sess+    -> V.Key SessionId+    -> SV.SessionVault (Session conn sess st)+    -> Wai.Middleware+sessionMiddleware pool cfg vK sessionRef app req respond =     case getCookieFromReq (sc_cookieName cfg) of       Just sid ->           do mSess <- loadSessionImpl cfg sessionRef sid@@ -241,15 +249,17 @@               then mapReqHeaders (addCookie sess) unwrappedResp               else unwrappedResp       mkNew =-          do newSess <- newSessionImpl cfg sessionRef defVal+          do newSess <- newSessionImpl pool cfg sessionRef defVal              withSess True newSess -newSessionImpl :: SessionCfg sess-               -> SV.SessionVault (Session conn sess st)-               -> sess-               -> IO (Session conn sess st)-newSessionImpl sessCfg sessionRef content =-    do sess <- createSession sessCfg content+newSessionImpl ::+    CR.EntropyPool+    -> SessionCfg sess+    -> SV.SessionVault (Session conn sess st)+    -> sess+    -> IO (Session conn sess st)+newSessionImpl pool sessCfg sessionRef content =+    do sess <- createSession pool sessCfg content        atomically $ SV.storeSession sess sessionRef        return $! sess @@ -286,14 +296,14 @@     atomically $ SV.deleteSession sid sessionRef  clearAllSessionsImpl :: SV.SessionVault (Session conn sess st)-                     -> SpockAction conn sess st ()+                     -> SpockActionCtx ctx conn sess st () clearAllSessionsImpl sessionRef =     liftIO $ atomically $ SV.filterSessions (const False) sessionRef  mapAllSessionsImpl ::     SV.SessionVault (Session conn sess st)     -> (sess -> STM sess)-    -> SpockAction conn sess st ()+    -> SpockActionCtx ctx conn sess st () mapAllSessionsImpl sessionRef f =     liftIO $ atomically $ flip SV.mapSessions sessionRef $ \sess ->         do newData <- f (sess_data sess)@@ -305,26 +315,29 @@                   -> IO () housekeepSessions cfg sessionRef storeSessions =     do now <- getCurrentTime-       newStatus <-+       (newStatus, oldStatus) <-            atomically $-           do SV.filterSessions (\sess -> sess_validUntil sess > now) sessionRef-              SV.toList sessionRef-       storeSessions (HM.fromList $ map (\v -> (SV.getSessionKey v, v)) newStatus)+           do oldSt <- SV.toList sessionRef+              SV.filterSessions (\sess -> sess_validUntil sess > now) sessionRef+              (,) <$> SV.toList sessionRef <*> pure oldSt+       let packSessionHm = HM.fromList . map (\v -> (SV.getSessionKey v, v))+           oldHm = packSessionHm oldStatus+           newHm = packSessionHm newStatus+       storeSessions newHm+       sh_removed (sc_hooks cfg) (HM.map sess_data $ oldHm `HM.difference` newHm)        threadDelay (1000 * 1000 * (round $ sc_housekeepingInterval cfg)) -createSession :: SessionCfg sess -> sess -> IO (Session conn sess st)-createSession sessCfg content =-    do sid <- randomHash (sc_sessionIdEntropy sessCfg)+createSession :: CR.EntropyPool -> SessionCfg sess -> sess -> IO (Session conn sess st)+createSession pool sessCfg content =+    do sid <- randomHash pool (sc_sessionIdEntropy sessCfg)        now <- getCurrentTime        let validUntil = addUTCTime (sc_sessionTTL sessCfg) now            emptySafeActions =                SafeActionStore HM.empty HM.empty        return (Session sid validUntil content emptySafeActions) -randomHash :: Int -> IO T.Text-randomHash len =-    do gen <- g-       return $ T.replace "=" "" $ T.decodeUtf8 $ B64.encode $ BSC.pack $-              take len $ randoms gen-    where-      g = newStdGen :: IO StdGen+randomHash :: CR.EntropyPool -> Int -> IO T.Text+randomHash pool len =+    do let sys :: CR.SystemRNG+           sys = CR.cprgCreate pool+       return $ T.replace "=" "" $ T.decodeUtf8 $ B64.encode $ fst $ CR.cprgGenerateWithEntropy len sys
src/Web/Spock/Internal/Types.hs view
@@ -1,15 +1,15 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE UndecidableInstances #-} module Web.Spock.Internal.Types where -import Web.Spock.Internal.CoreAction import Web.Spock.Internal.Wire  #if MIN_VERSION_base(4,8,0)@@ -34,10 +34,13 @@ type SpockAllM r conn sess st a =     SpockAllT r (WebStateM conn sess st) a --- | The SpockAction is the monad of all route-actions. You have access--- to the database, session and state of your application.-type SpockAction conn sess st = ActionT (WebStateM conn sess st)+-- | The 'SpockActionCtx' is the monad of all route-actions. You have access+-- to the context of the request and database, session and state of your application.+type SpockActionCtx ctx conn sess st = ActionCtxT ctx (WebStateM conn sess st) +-- | The 'SpockAction' is a specialisation of 'SpockActionCtx' with a '()' context.+type SpockAction conn sess st = SpockActionCtx () conn sess st+ -- | Spock configuration data SpockCfg conn sess st    = SpockCfg@@ -80,9 +83,10 @@  -- | You can feed Spock with either a connection pool, or instructions on how to build -- a connection pool. See 'ConnBuilder'-data PoolOrConn a-   = PCPool (Pool a)-   | PCConn (ConnBuilder a)+data PoolOrConn a where+    PCPool :: Pool a -> PoolOrConn a+    PCConn :: ConnBuilder a -> PoolOrConn a+    PCNoDatabase :: PoolOrConn ()  -- | Session configuration with reasonable defaults defaultSessionCfg :: a -> SessionCfg a@@ -95,6 +99,7 @@     , sc_emptySession = emptySession     , sc_persistCfg = Nothing     , sc_housekeepingInterval = 60 * 10+    , sc_hooks = defaultSessionHooks     }  -- | Configuration for the session manager@@ -114,8 +119,23 @@      -- ^ persistence interface for sessions    , sc_housekeepingInterval :: NominalDiffTime      -- ^ how often should the session manager check for dangeling dead sessions+   , sc_hooks :: SessionHooks a+     -- ^ hooks into the session manager    } +-- | NOP session hooks+defaultSessionHooks :: SessionHooks a+defaultSessionHooks =+    SessionHooks+    { sh_removed = const $ return ()+    }++-- | Hook into the session manager to trigger custom behavior+data SessionHooks a+   = SessionHooks+   { sh_removed :: HM.HashMap SessionId a -> IO ()+   }+ data SessionPersistCfg a    = SessionPersistCfg    { spc_load :: IO [(SessionId, UTCTime, a)]@@ -197,15 +217,15 @@  data SessionManager conn sess st    = SessionManager-   { sm_getSessionId :: SpockAction conn sess st SessionId-   , sm_readSession :: SpockAction conn sess st sess-   , sm_writeSession :: sess -> SpockAction conn sess st ()-   , sm_modifySession :: forall a. (sess -> (sess, a)) -> SpockAction conn sess st a-   , sm_mapSessions :: (sess -> STM sess) -> SpockAction conn sess st ()-   , sm_clearAllSessions :: SpockAction conn sess st ()+   { sm_getSessionId :: forall ctx. SpockActionCtx ctx conn sess st SessionId+   , sm_readSession :: forall ctx. SpockActionCtx ctx conn sess st sess+   , sm_writeSession :: forall ctx. sess -> SpockActionCtx ctx conn sess st ()+   , sm_modifySession :: forall a ctx. (sess -> (sess, a)) -> SpockActionCtx ctx conn sess st a+   , sm_mapSessions :: forall ctx. (sess -> STM sess) -> SpockActionCtx ctx conn sess st ()+   , sm_clearAllSessions :: forall ctx. SpockActionCtx ctx conn sess st ()    , sm_middleware :: Middleware-   , sm_addSafeAction :: PackedSafeAction conn sess st -> SpockAction conn sess st SafeActionHash-   , sm_lookupSafeAction :: SafeActionHash -> SpockAction conn sess st (Maybe (PackedSafeAction conn sess st))-   , sm_removeSafeAction :: PackedSafeAction conn sess st -> SpockAction conn sess st ()+   , sm_addSafeAction :: forall ctx. PackedSafeAction conn sess st -> SpockActionCtx ctx conn sess st SafeActionHash+   , sm_lookupSafeAction :: forall ctx. SafeActionHash -> SpockActionCtx ctx conn sess st (Maybe (PackedSafeAction conn sess st))+   , sm_removeSafeAction :: forall ctx. PackedSafeAction conn sess st -> SpockActionCtx ctx conn sess st ()    , sm_closeSessionManager :: IO ()    }
src/Web/Spock/Internal/Wire.hs view
@@ -1,3 +1,4 @@+{-# OPTIONS_GHC -fno-warn-orphans #-} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE CPP #-} {-# LANGUAGE DeriveDataTypeable #-}@@ -66,13 +67,15 @@    , vi_lookupKey :: forall a. V.Key a -> IO (Maybe a)    } -data RequestInfo+data RequestInfo ctx    = RequestInfo-   { ri_request :: Wai.Request-   , ri_params :: HM.HashMap CaptureVar T.Text+   { ri_method :: !StdMethod+   , ri_request :: !Wai.Request+   , ri_params :: !(HM.HashMap CaptureVar T.Text)    , ri_queryParams :: [(T.Text, T.Text)]-   , ri_files :: HM.HashMap T.Text UploadedFile-   , ri_vaultIf :: VaultIf+   , ri_files :: !(HM.HashMap T.Text UploadedFile)+   , ri_vaultIf :: !VaultIf+   , ri_context :: !ctx    }  newtype ResponseBody = ResponseBody (Status -> ResponseHeaders -> Wai.Response)@@ -147,13 +150,15 @@     strMsg = ActionError #endif -newtype ActionT m a-    = ActionT { runActionT :: ErrorT ActionInterupt (RWST RequestInfo () ResponseState m) a }-      deriving (Monad, Functor, Applicative, Alternative, MonadIO, MonadReader RequestInfo, MonadState ResponseState, MonadError ActionInterupt)+type ActionT = ActionCtxT () -instance MonadTrans ActionT where-    lift = ActionT . lift . lift+newtype ActionCtxT ctx m a+    = ActionCtxT { runActionCtxT :: ErrorT ActionInterupt (RWST (RequestInfo ctx) () ResponseState m) a }+      deriving (Monad, Functor, Applicative, Alternative, MonadIO, MonadReader (RequestInfo ctx), MonadState ResponseState, MonadError ActionInterupt) +instance MonadTrans (ActionCtxT ctx) where+    lift = ActionCtxT . lift . lift+ respStateToResponse :: ResponseState -> Wai.Response respStateToResponse (ResponseState headers multiHeaders status (ResponseBody body)) =     let mkMultiHeader (k, vals) =@@ -209,8 +214,8 @@       fallbackApp :: Wai.Application       fallbackApp _ respond = respond notFound -makeActionEnvironment :: InternalState -> Wai.Request -> IO (ParamMap -> RequestInfo, TVar V.Vault, IO ())-makeActionEnvironment st req =+makeActionEnvironment :: InternalState -> StdMethod -> Wai.Request -> IO (ParamMap -> RequestInfo (), TVar V.Vault, IO ())+makeActionEnvironment st stdMethod req =     do (bodyParams, bodyFiles) <- P.parseRequestBody (P.tempFileBackEnd st) req        vaultVar <- liftIO $ newTVarIO (Wai.vault req)        let vaultIf =@@ -232,11 +237,13 @@            queryParams = postParams ++ getParams        return ( \params ->                     RequestInfo-                    { ri_request = req+                    { ri_method = stdMethod+                    , ri_request = req                     , ri_params = params                     , ri_queryParams = queryParams                     , ri_files = uploadedFiles                     , ri_vaultIf = vaultIf+                    , ri_context = ()                     }               , vaultVar               , removeUploadedFiles uploadedFiles@@ -250,7 +257,7 @@  applyAction :: MonadIO m             => Wai.Request-            -> (ParamMap -> RequestInfo)+            -> (ParamMap -> RequestInfo ())             -> [(ParamMap, ActionT m ())]             -> m (Maybe ResponseState) applyAction _ _ [] =@@ -259,7 +266,7 @@     do let env = mkEnv captures            defResp = errorResponse status200 ""        (r, respState, _) <--           runRWST (runErrorT $ runActionT selectedAction) env defResp+           runRWST (runErrorT $ runActionCtxT selectedAction) env defResp        case r of          Left (ActionRedirect loc) ->              return $ Just $@@ -284,25 +291,29 @@  handleRequest     :: MonadIO m-    => Maybe Word64+    => StdMethod+    -> Maybe Word64     -> (forall a. m a -> IO a)     -> [(ParamMap, ActionT m ())]     -> InternalState     -> Wai.Application -> Wai.Application-handleRequest mLimit registryLift allActions st coreApp req respond =+handleRequest stdMethod mLimit registryLift allActions st coreApp req respond =     do reqGo <-            case mLimit of              Nothing -> return req              Just lim -> requestSizeCheck lim req-       handleRequest' registryLift allActions st coreApp reqGo respond+       handleRequest' stdMethod registryLift allActions st coreApp reqGo respond -handleRequest' :: MonadIO m => (forall a. m a -> IO a)-               -> [(ParamMap, ActionT m ())]-               -> InternalState-               -> Wai.Application -> Wai.Application-handleRequest' registryLift allActions st coreApp req respond =+handleRequest' ::+    MonadIO m+    => StdMethod+    -> (forall a. m a -> IO a)+    -> [(ParamMap, ActionT m ())]+    -> InternalState+    -> Wai.Application -> Wai.Application+handleRequest' stdMethod registryLift allActions st coreApp req respond =     do actEnv <--           (Left <$> makeActionEnvironment st req)+           (Left <$> makeActionEnvironment st stdMethod req)            `catch` \(_ :: SizeException) ->                return (Right sizeError)        case actEnv of@@ -365,5 +376,5 @@               Right stdMethod ->                   do let allActions = getMatchingRoutes stdMethod (Wai.pathInfo req)                      runResourceT $ withInternalState $ \st ->-                         handleRequest mLimit registryLift allActions st coreApp req respond+                         handleRequest stdMethod mLimit registryLift allActions st coreApp req respond        return $ spockMiddleware . app
src/Web/Spock/Safe.hs view
@@ -13,14 +13,14 @@ -} module Web.Spock.Safe     ( -- * Spock's route definition monad-      spock, SpockM-    , spockT, spockLimT, SpockT+      spock, SpockM, SpockCtxM+    , spockT, spockLimT, SpockT, SpockCtxT      -- * Defining routes     , Path, root, Var, var, static, (<//>)      -- * Rendering routes     , renderRoute      -- * Hooking routes-    , subcomponent+    , subcomponent, prehook     , get, post, getpost, head, put, delete, patch, hookRoute, hookAny     , Http.StdMethod (..)       -- * Adding Wai.Middleware@@ -38,34 +38,46 @@ import qualified Web.Spock.Internal.Core as C  import Control.Applicative-import Control.Monad.Trans+import Control.Monad.Reader import Data.HVect hiding (head) import Data.Monoid import Data.Word import Network.HTTP.Types.Method-import Prelude hiding (head)+import Prelude hiding (head, uncurry, curry)+import Web.Routing.AbstractRouter (swapMonad) import Web.Routing.SafeRouting hiding (renderRoute) import qualified Data.Text as T import qualified Network.HTTP.Types as Http import qualified Network.Wai as Wai import qualified Web.Routing.SafeRouting as SR -type SpockM conn sess st a = SpockT (WebStateM conn sess st) a+type SpockM conn sess st = SpockCtxM () conn sess st+type SpockCtxM ctx conn sess st = SpockCtxT ctx (WebStateM conn sess st) -newtype SpockT m a-    = SpockT { runSpockT :: C.SpockAllT (SafeRouter (ActionT m) ()) m a-             } deriving (Monad, Functor, Applicative, MonadIO)+type SpockT = SpockCtxT () -instance MonadTrans SpockT where-    lift = SpockT . lift+newtype LiftHooked ctx m =+    LiftHooked { unLiftHooked :: forall a. ActionCtxT ctx m a -> ActionCtxT () m a } +injectHook :: LiftHooked ctx m -> (forall a. ActionCtxT ctx' m a -> ActionCtxT ctx m a) -> LiftHooked ctx' m+injectHook (LiftHooked baseHook) nextHook =+    LiftHooked $ baseHook . nextHook++newtype SpockCtxT ctx m a+    = SpockCtxT+    { runSpockT :: C.SpockAllT (SafeRouter (ActionT m) ()) (ReaderT (LiftHooked ctx m) m) a+    } deriving (Monad, Functor, Applicative, MonadIO)++instance MonadTrans (SpockCtxT ctx) where+    lift = SpockCtxT . lift . lift+ -- | Create a spock application using a given db storageLayer and an initial state. -- Spock works with database libraries that already implement connection pooling and -- with those that don't come with it out of the box. For more see the 'PoolOrConn' type. -- Use @runSpock@ to run the app or @spockAsApp@ to create a @Wai.Application@ spock :: SpockCfg conn sess st -> SpockM conn sess st () -> IO Wai.Middleware spock spockCfg spockAppl =-    C.spockAll SafeRouter spockCfg (runSpockT spockAppl')+    C.spockAll SafeRouter spockCfg (baseAppHook spockAppl')     where       spockAppl' =           do hookSafeActions@@ -81,50 +93,79 @@ spockT = spockLimT Nothing  -- | Like @spockT@, but first argument is request size limit in bytes. Set to 'Nothing' to disable.-spockLimT :: (MonadIO m)+spockLimT :: forall m .MonadIO m        => Maybe Word64        -> (forall a. m a -> IO a)        -> SpockT m ()        -> IO Wai.Middleware-spockLimT mSizeLimit liftFun (SpockT app) =-    C.spockAllT mSizeLimit SafeRouter liftFun app+spockLimT mSizeLimit liftFun app =+    C.spockAllT mSizeLimit SafeRouter liftFun (baseAppHook app) +baseAppHook :: forall m. MonadIO m => SpockT m () -> C.SpockAllT (SafeRouter (ActionT m) ()) m ()+baseAppHook app =+    swapMonad lifter (runSpockT app)+    where+      lifter :: forall b. ReaderT (LiftHooked () m) m b -> m b+      lifter action = runReaderT action (LiftHooked id)+ -- | Specify an action that will be run when the HTTP verb 'GET' and the given route match-get :: MonadIO m => Path xs -> HVectElim xs (ActionT m ()) -> SpockT m ()+get :: (HasRep xs, MonadIO m) => Path xs -> HVectElim xs (ActionCtxT ctx m ()) -> SpockCtxT ctx m () get = hookRoute GET  -- | Specify an action that will be run when the HTTP verb 'POST' and the given route match-post :: MonadIO m => Path xs -> HVectElim xs (ActionT m ()) -> SpockT m ()+post :: (HasRep xs, MonadIO m) => Path xs -> HVectElim xs (ActionCtxT ctx m ()) -> SpockCtxT ctx m () post = hookRoute POST  -- | Specify an action that will be run when the HTTP verb 'GET'/'POST' and the given route match-getpost :: MonadIO m => Path xs -> HVectElim xs (ActionT m ()) -> SpockT m ()+getpost :: (HasRep xs, MonadIO m) => Path xs -> HVectElim xs (ActionCtxT ctx m ()) -> SpockCtxT ctx m () getpost r a = hookRoute POST r a >> hookRoute GET r a  -- | Specify an action that will be run when the HTTP verb 'HEAD' and the given route match-head :: MonadIO m => Path xs -> HVectElim xs (ActionT m ()) -> SpockT m ()+head :: (HasRep xs, MonadIO m) => Path xs -> HVectElim xs (ActionCtxT ctx m ()) -> SpockCtxT ctx m () head = hookRoute HEAD  -- | Specify an action that will be run when the HTTP verb 'PUT' and the given route match-put :: MonadIO m => Path xs -> HVectElim xs (ActionT m ()) -> SpockT m ()+put :: (HasRep xs, MonadIO m) => Path xs -> HVectElim xs (ActionCtxT ctx m ()) -> SpockCtxT ctx m () put = hookRoute PUT  -- | Specify an action that will be run when the HTTP verb 'DELETE' and the given route match-delete :: MonadIO m => Path xs -> HVectElim xs (ActionT m ()) -> SpockT m ()+delete :: (HasRep xs, MonadIO m) => Path xs -> HVectElim xs (ActionCtxT ctx m ()) -> SpockCtxT ctx m () delete = hookRoute DELETE  -- | Specify an action that will be run when the HTTP verb 'PATCH' and the given route match-patch :: MonadIO m => Path xs -> HVectElim xs (ActionT m ()) -> SpockT m ()+patch :: (HasRep xs, MonadIO m) => Path xs -> HVectElim xs (ActionCtxT ctx m ()) -> SpockCtxT ctx m () patch = hookRoute PATCH +-- | Specify an action that will be run before all subroutes. It can modify the requests current context+prehook :: forall m ctx ctx'. MonadIO m => ActionCtxT ctx m ctx' -> SpockCtxT ctx' m () -> SpockCtxT ctx m ()+prehook hook (SpockCtxT hookBody) =+    SpockCtxT $+    do prevHook <- lift ask+       let newHook :: ActionCtxT ctx' m a -> ActionCtxT ctx m a+           newHook act =+               do newCtx <- hook+                  runInContext newCtx act+           hookLift :: forall a. ReaderT (LiftHooked ctx' m) m a -> ReaderT (LiftHooked ctx m) m a+           hookLift a =+               lift $ runReaderT a (injectHook prevHook newHook)+       swapMonad hookLift hookBody+ -- | Specify an action that will be run when a HTTP verb and the given route match-hookRoute :: Monad m => StdMethod -> Path xs -> HVectElim xs (ActionT m ()) -> SpockT m ()-hookRoute m path action = SpockT $ C.hookRoute m (SafeRouterPath path) (HVectElim' action)+hookRoute :: forall xs ctx m. (HasRep xs, Monad m) => StdMethod -> Path xs -> HVectElim xs (ActionCtxT ctx m ()) -> SpockCtxT ctx m ()+hookRoute m path action =+    SpockCtxT $+    do hookLift <- lift $ asks unLiftHooked+       let actionPacker :: HVectElim xs (ActionCtxT ctx m ()) -> HVect xs -> ActionCtxT () m ()+           actionPacker act captures = hookLift (uncurry act captures)+       C.hookRoute m (SafeRouterPath path) (HVectElim' $ curry $ actionPacker action)  -- | Specify an action that will be run when a HTTP verb matches but no defined route matches. -- The full path is passed as an argument-hookAny :: Monad m => StdMethod -> ([T.Text] -> ActionT m ()) -> SpockT m ()-hookAny m action = SpockT $ C.hookAny m action+hookAny :: Monad m => StdMethod -> ([T.Text] -> ActionCtxT ctx m ()) -> SpockCtxT ctx m ()+hookAny m action =+    SpockCtxT $+    do hookLift <- lift $ asks unLiftHooked+       C.hookAny m (hookLift . action)  -- | Define a subcomponent. Usage example: --@@ -136,12 +177,12 @@ -- -- The request \/site\/home will be routed to homeHandler and the -- request \/admin\/home will be routed to adminHomeHandler-subcomponent :: Monad m => Path '[] -> SpockT m () -> SpockT m ()-subcomponent p (SpockT subapp) = SpockT $ C.subcomponent (SafeRouterPath p) subapp+subcomponent :: Monad m => Path '[] -> SpockCtxT ctx m () -> SpockCtxT ctx m ()+subcomponent p (SpockCtxT subapp) = SpockCtxT $ C.subcomponent (SafeRouterPath p) subapp  -- | Hook wai middleware into Spock-middleware :: Monad m => Wai.Middleware -> SpockT m ()-middleware = SpockT . C.middleware+middleware :: Monad m => Wai.Middleware -> SpockCtxT ctx m ()+middleware = SpockCtxT . C.middleware  -- | Wire up a safe action: Safe actions are actions that are protected from -- csrf attacks. Here's a usage example:
src/Web/Spock/Shared.hs view
@@ -9,13 +9,15 @@     (-- * Helpers for running Spock       runSpock, spockAsApp      -- * Action types-    , SpockAction, ActionT+    , SpockAction, SpockActionCtx, ActionT, W.ActionCtxT      -- * Handling requests-    , request, header, rawHeader, cookie+    , request, header, rawHeader, cookie, reqMethod     , preferredFormat, ClientPreferredFormat(..)     , body, jsonBody, jsonBody'     , files, UploadedFile (..)     , params, param, param'+     -- * Working with context+    , getContext, runInContext      -- * Sending responses     , setStatus, setHeader, redirect, jumpNext, setCookie, setCookie', deleteCookie, bytes, lazyBytes     , text, html, file, json, stream, response@@ -31,6 +33,7 @@     , requireBasicAuth      -- * Sessions     , defaultSessionCfg, SessionCfg (..)+    , defaultSessionHooks, SessionHooks (..)     , SessionPersistCfg(..), readShowSessionPersist     , SessionId     , getSessionId, readSession, writeSession@@ -65,51 +68,51 @@  -- | Get the current users sessionId. Note that this ID should only be -- shown to it's owner as otherwise sessions can be hijacked.-getSessionId :: SpockAction conn sess st SessionId+getSessionId :: SpockActionCtx ctx conn sess st SessionId getSessionId =     getSessMgr >>= sm_getSessionId  -- | Write to the current session. Note that all data is stored on the server. -- The user only reciedes a sessionId to be identified.-writeSession :: sess -> SpockAction conn sess st ()+writeSession :: sess -> SpockActionCtx ctx conn sess st () writeSession d =     do mgr <- getSessMgr        sm_writeSession mgr d  -- | Modify the stored session-modifySession :: (sess -> sess) -> SpockAction conn sess st ()+modifySession :: (sess -> sess) -> SpockActionCtx ctx conn sess st () modifySession f =     modifySession' $ \sess -> (f sess, ())  -- | Modify the stored session and return a value-modifySession' :: (sess -> (sess, a)) -> SpockAction conn sess st a+modifySession' :: (sess -> (sess, a)) -> SpockActionCtx ctx conn sess st a modifySession' f =     do mgr <- getSessMgr        sm_modifySession mgr f  -- | Modify the stored session and return the new value after modification-modifyReadSession :: (sess -> sess) -> SpockAction conn sess st sess+modifyReadSession :: (sess -> sess) -> SpockActionCtx ctx conn sess st sess modifyReadSession f =     modifySession' $ \sess ->         let x = f sess         in (x, x)  -- | Read the stored session-readSession :: SpockAction conn sess st sess+readSession :: SpockActionCtx ctx conn sess st sess readSession =     do mgr <- getSessMgr        sm_readSession mgr  -- | Globally delete all existing sessions. This is useful for example if you want -- to require all users to relogin-clearAllSessions :: SpockAction conn sess st ()+clearAllSessions :: SpockActionCtx ctx conn sess st () clearAllSessions =     do mgr <- getSessMgr        sm_clearAllSessions mgr  -- | Apply a transformation to all sessions. Be careful with this, as this -- may cause many STM transaction retries.-mapAllSessions :: (sess -> STM sess) -> SpockAction conn sess st ()+mapAllSessions :: (sess -> STM sess) -> SpockActionCtx ctx conn sess st () mapAllSessions f =     do mgr <- getSessMgr        sm_mapSessions mgr f
test/Web/Spock/FrameworkSpecHelper.hs view
@@ -38,6 +38,7 @@     with app $     do routingSpec        actionSpec+       headerTest        cookieTest  routingSpec :: SpecWith Wai.Application@@ -99,6 +100,26 @@                   , matchHeaders =                       [ matchCookie "multiple1" "test1"                       , matchCookie "multiple2" "test2"+                      ]+                  }+headerTest :: SpecWith Wai.Application+headerTest =+    describe "Headers" $+    do it "supports custom headers" $+          get "/set-header" `shouldRespondWith`+                  "ok"+                  { matchStatus = 200+                  , matchHeaders =+                      [ "X-FooBar" <:> "Baz"+                      ]+                  }+       it "supports multi headers" $+          get "/set-multi-header" `shouldRespondWith`+                  "ok"+                  { matchStatus = 200+                  , matchHeaders =+                      [ "Content-Language" <:> "de"+                      , "Content-Language" <:> "en"                       ]                   } 
test/Web/Spock/SafeSpec.hs view
@@ -1,13 +1,22 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE DoAndIfThenElse #-} module Web.Spock.SafeSpec (spec) where  import Web.Spock.Safe import Web.Spock.FrameworkSpecHelper +import Control.Arrow (second)+import Control.Monad+import Data.IORef+import Data.List (find) import Data.Monoid import Test.Hspec+import qualified Data.HashSet as HS import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import qualified Network.Wai.Test as Wai+import qualified Test.Hspec.Wai as Test  app :: SpockT IO () app =@@ -31,6 +40,13 @@            do setCookie "multiple1" "test1" 3600               setCookie "multiple2" "test2" 3600               text "set"+       get "set-header" $+           do setHeader "X-FooBar" "Baz"+              text "ok"+       get "set-multi-header" $+           do setHeader "Content-Language" "de"+              setHeader "Content-Language" "en"+              text "ok"        subcomponent "/subcomponent" $          do get "foo" $ text "foo"             subcomponent "/subcomponent2" $@@ -58,10 +74,85 @@              let r3 = "blog" <//> (var :: Var Int) <//> (var :: Var T.Text)              renderRoute r3 2 "BIIM" `shouldBe` "/blog/2/BIIM" +ctxApp :: SpockT IO ()+ctxApp =+    prehook hook $+    do get "test" $ getContext >>= text+       post "test" $ getContext >>= text+    where+      hook =+          do sid <- header "X-ApiKey"+             case sid of+               Just s -> return s+               Nothing -> text "Missing ApiKey"++ctxSpec :: Spec+ctxSpec =+    describe "Contexts" $+    Test.with (spockAsApp $ spockT id ctxApp) $+    it "should work" $+       do Test.request "GET" "/test" [] "" `Test.shouldRespondWith` "Missing ApiKey"+          Test.request "GET" "/test" [("X-ApiKey", "foo")] "" `Test.shouldRespondWith` "foo"+          Test.request "POST" "/test" [("X-ApiKey", "foo")] "" `Test.shouldRespondWith` "foo"++sessionSpec :: Spec+sessionSpec =+    describe "SessionManager" $+    Test.with sessionApp $+    do it "should generate random session ids" $+          do ids <- Test.liftIO $ newIORef HS.empty+             -- of course this sample is too small to prove anything, but it's a good+             -- smoke test...+             replicateM_ 500 $+                do res <- Test.get "/test"+                   Test.liftIO $ checkCookie ids res `shouldReturn` True+       it "should remember a session" $+          do res <- Test.get "/set/5"+             case getSessCookie res of+               Nothing ->+                   Test.liftIO $ expectationFailure "Missing spockcookie"+               Just sessCookie ->+                   Test.request "GET" "/check" [("Cookie", T.encodeUtf8 sessCookie)] ""+                           `Test.shouldRespondWith` "5"+    where+      sessionApp =+          spockAsApp $+          spock spockCfg $+          do get "test" $ text "text"+             get ("set" <//> var) $ \number -> writeSession number >> text "done"+             get "check" $+                 do val <- readSession+                    text (T.pack $ show val)+      spockCfg =+          defaultSpockCfg (0 :: Int) PCNoDatabase True++      getSessCookie :: Wai.SResponse -> Maybe T.Text+      getSessCookie resp =+          let headers = Wai.simpleHeaders resp+          in fmap snd $ find (\h -> fst h == "Set-Cookie" && "spockcookie" `T.isPrefixOf` snd h) $+             map (second T.decodeUtf8) headers++      checkCookie :: IORef (HS.HashSet T.Text) -> Wai.SResponse -> IO Bool+      checkCookie setRef resp =+          do let mSessCookie = getSessCookie resp+             case mSessCookie of+               Nothing ->+                   do expectationFailure "Missing spockcookie"+                      return False+               Just sessCookie ->+                   let (_, cval) = T.breakOn "=" $ fst $ T.breakOn ";" sessCookie+                   in do set <- readIORef setRef+                         if HS.member cval set+                         then return False+                         else do writeIORef setRef $ HS.insert cval set+                                 return True+ spec :: Spec spec =     describe "SafeRouting" $     do frameworkSpec (spockAsApp $ spockT id app)+       ctxSpec+       sessionSpec        routeRenderingSpec        sizeLimitSpec $ \lim -> spockAsApp $ spockLimT (Just lim) id $           post "size" $ body >>= bytes
test/Web/Spock/SimpleSpec.hs view
@@ -41,6 +41,13 @@            do setCookie "multiple1" "test1" 3600               setCookie "multiple2" "test2" 3600               text "set"+       get "set-header" $+           do setHeader "X-FooBar" "Baz"+              text "ok"+       get "set-multi-header" $+           do setHeader "Content-Language" "de"+              setHeader "Content-Language" "en"+              text "ok"        hookAny GET $ text . T.intercalate "/"  spec :: Spec