packages feed

snap 0.5.5.1 → 0.6.0

raw patch · 60 files changed

+5257/−1747 lines, 60 filesdep +Cryptodep +aesondep +clientsessiondep −blaze-builderdep −bytestring-numsdep −dlistdep ~MonadCatchIO-transformersdep ~attoparsecdep ~bytestringnew-uploaderPVP ok

version bump matches the API change (PVP)

Dependencies added: Crypto, aeson, clientsession, configurator, data-lens, data-lens-template, hashable, logict, mwc-random, pwstore-fast, safe, stm, syb, transformers, unordered-containers, utf8-string, vector, vector-algorithms, xmlhtml

Dependencies removed: blaze-builder, bytestring-nums, dlist, enumerator, old-locale, unix-compat, zlib

Dependency ranges changed: MonadCatchIO-transformers, attoparsec, bytestring, containers, directory-tree, heist, mtl, old-time, snap-core, snap-server, template-haskell, time

API changes (from Hackage documentation)

- Snap.Extension: class InitializerState s
- Snap.Extension: data Initializer s
- Snap.Extension: data SnapExtend s a
- Snap.Extension: defaultReloadHandler :: MonadSnap m => IO [(ByteString, Maybe ByteString)] -> m ()
- Snap.Extension: extensionId :: InitializerState s => s -> ByteString
- Snap.Extension: instance Alternative (SnapExtend s)
- Snap.Extension: instance Applicative (SnapExtend s)
- Snap.Extension: instance Applicative Initializer
- Snap.Extension: instance Functor (SnapExtend s)
- Snap.Extension: instance Functor Initializer
- Snap.Extension: instance Monad (SnapExtend s)
- Snap.Extension: instance Monad Initializer
- Snap.Extension: instance MonadCatchIO (SnapExtend s)
- Snap.Extension: instance MonadIO (SnapExtend s)
- Snap.Extension: instance MonadIO Initializer
- Snap.Extension: instance MonadPlus (SnapExtend s)
- Snap.Extension: instance MonadReader s (SnapExtend s)
- Snap.Extension: instance MonadSnap (SnapExtend s)
- Snap.Extension: mkCleanup :: InitializerState s => s -> IO ()
- Snap.Extension: mkInitializer :: InitializerState s => s -> Initializer s
- Snap.Extension: mkReload :: InitializerState s => s -> IO ()
- Snap.Extension: nullReloadHandler :: MonadSnap m => IO [(ByteString, Maybe ByteString)] -> m ()
- Snap.Extension: runInitializer :: Bool -> Initializer s -> SnapExtend s () -> IO (Snap (), IO (), IO [(ByteString, Maybe ByteString)])
- Snap.Extension: runInitializerWithReloadAction :: Bool -> Initializer s -> SnapExtend s () -> (IO [(ByteString, Maybe ByteString)] -> SnapExtend s ()) -> IO (Snap (), IO ())
- Snap.Extension: runInitializerWithoutReloadAction :: Initializer s -> SnapExtend s () -> IO (Snap (), IO ())
- Snap.Extension.Heist: class (Monad n, MonadSnap m) => MonadHeist n m | m -> n
- Snap.Extension.Heist: heistLocal :: MonadHeist n m => (TemplateState n -> TemplateState n) -> m a -> m a
- Snap.Extension.Heist: heistServe :: MonadHeist n m => m ()
- Snap.Extension.Heist: heistServeSingle :: MonadHeist n m => ByteString -> m ()
- Snap.Extension.Heist: render :: MonadHeist n m => ByteString -> m ()
- Snap.Extension.Heist: renderAs :: MonadHeist n m => ByteString -> ByteString -> m ()
- Snap.Extension.Heist: renderWithSplices :: MonadHeist n m => ByteString -> [(Text, Splice n)] -> m ()
- Snap.Extension.Heist.Impl: class MonadSnap m => HasHeistState m s | s -> m
- Snap.Extension.Heist.Impl: class (Monad n, MonadSnap m) => MonadHeist n m | m -> n
- Snap.Extension.Heist.Impl: data MonadSnap m => HeistState m
- Snap.Extension.Heist.Impl: getHeistState :: HasHeistState m s => s -> HeistState m
- Snap.Extension.Heist.Impl: heistInitializer :: MonadSnap m => FilePath -> (TemplateState m -> TemplateState m) -> Initializer (HeistState m)
- Snap.Extension.Heist.Impl: heistLocal :: MonadHeist n m => (TemplateState n -> TemplateState n) -> m a -> m a
- Snap.Extension.Heist.Impl: heistServe :: MonadHeist n m => m ()
- Snap.Extension.Heist.Impl: heistServeSingle :: MonadHeist n m => ByteString -> m ()
- Snap.Extension.Heist.Impl: instance HasHeistState (SnapExtend s) s => MonadHeist (SnapExtend s) (SnapExtend s)
- Snap.Extension.Heist.Impl: instance HasHeistState m s => MonadHeist m (ReaderT s m)
- Snap.Extension.Heist.Impl: instance MonadSnap m => InitializerState (HeistState m)
- Snap.Extension.Heist.Impl: modifyHeistState :: HasHeistState m s => (HeistState m -> HeistState m) -> s -> s
- Snap.Extension.Heist.Impl: registerSplices :: (MonadSnap m, MonadIO n) => HeistState m -> [(Text, Splice m)] -> n ()
- Snap.Extension.Heist.Impl: render :: MonadHeist n m => ByteString -> m ()
- Snap.Extension.Heist.Impl: renderAs :: MonadHeist n m => ByteString -> ByteString -> m ()
- Snap.Extension.Heist.Impl: setHeistState :: HasHeistState m s => HeistState m -> s -> s
- Snap.Extension.Loader.Devel: loadSnapTH :: Name -> Name -> [String] -> Q Exp
- Snap.Extension.Loader.Devel: loadSnapTH' :: [String] -> [String] -> [String] -> String -> Q Exp
- Snap.Extension.Server: defaultConfig :: ConfigExtend s
- Snap.Extension.Server: getReloadHandler :: ConfigExtend s -> Maybe (IO [(ByteString, Maybe ByteString)] -> SnapExtend s ())
- Snap.Extension.Server: httpServe :: ConfigExtend s -> Initializer s -> SnapExtend s () -> IO ()
- Snap.Extension.Server: quickHttpServe :: Initializer s -> SnapExtend s () -> IO ()
- Snap.Extension.Server: setReloadHandler :: (IO [(ByteString, Maybe ByteString)] -> SnapExtend s ()) -> ConfigExtend s -> ConfigExtend s
- Snap.Extension.Server: type ConfigExtend s = Config (SnapExtend s) (IO [(ByteString, Maybe ByteString)] -> SnapExtend s ())
+ Snap.Loader.Prod: loadSnapTH :: Q Exp -> Name -> [String] -> Q Exp
+ Snap.Snaplet: addPostInitHook :: (v -> IO v) -> Initializer b v ()
+ Snap.Snaplet: addPostInitHookBase :: (Snaplet b -> IO (Snaplet b)) -> Initializer b v ()
+ Snap.Snaplet: addRoutes :: [(ByteString, Handler b v ())] -> Initializer b v ()
+ Snap.Snaplet: class MonadSnaplet m
+ Snap.Snaplet: combineConfig :: Config Snap a -> Snap () -> IO (Config Snap a, Snap ())
+ Snap.Snaplet: data Handler b v a
+ Snap.Snaplet: data Initializer b v a
+ Snap.Snaplet: data Snaplet s
+ Snap.Snaplet: data SnapletConfig
+ Snap.Snaplet: data SnapletInit b v
+ Snap.Snaplet: embedSnaplet :: ByteString -> (Lens v (Snaplet v1)) -> SnapletInit v1 v1 -> Initializer b v (Snaplet v1)
+ Snap.Snaplet: getLens :: MonadSnaplet m => m b v (Lens (Snaplet b) (Snaplet v))
+ Snap.Snaplet: getOpaqueConfig :: MonadSnaplet m => m b v SnapletConfig
+ Snap.Snaplet: getSnapletAncestry :: (Monad (m b v), MonadSnaplet m) => m b v [Text]
+ Snap.Snaplet: getSnapletDescription :: (Monad (m b v), MonadSnaplet m) => m b v Text
+ Snap.Snaplet: getSnapletFilePath :: (Monad (m b v), MonadSnaplet m) => m b v FilePath
+ Snap.Snaplet: getSnapletName :: (Monad (m b v), MonadSnaplet m) => m b v (Maybe Text)
+ Snap.Snaplet: getSnapletRootURL :: (Monad (m b v), MonadSnaplet m) => m b v ByteString
+ Snap.Snaplet: getSnapletState :: Handler b v (Snaplet v)
+ Snap.Snaplet: getSnapletUserConfig :: (Monad (m b v), MonadSnaplet m) => m b v Config
+ Snap.Snaplet: getsSnapletState :: (Snaplet v -> b) -> Handler b1 v b
+ Snap.Snaplet: makeSnaplet :: Text -> Text -> Maybe (IO FilePath) -> Initializer b v v -> SnapletInit b v
+ Snap.Snaplet: modifySnapletState :: (Snaplet v -> Snaplet v) -> Handler b v ()
+ Snap.Snaplet: nameSnaplet :: Text -> SnapletInit b v -> SnapletInit b v
+ Snap.Snaplet: nestSnaplet :: ByteString -> (Lens v (Snaplet v1)) -> SnapletInit b v1 -> Initializer b v (Snaplet v1)
+ Snap.Snaplet: onUnload :: IO () -> Initializer b v ()
+ Snap.Snaplet: printInfo :: Text -> Initializer b v ()
+ Snap.Snaplet: putSnapletState :: Snaplet v -> Handler b v ()
+ Snap.Snaplet: reloadSite :: Handler b v ()
+ Snap.Snaplet: runSnaplet :: SnapletInit b b -> IO (Text, Snap (), IO ())
+ Snap.Snaplet: serveSnaplet :: Config Snap a -> SnapletInit b b -> IO ()
+ Snap.Snaplet: snapletConfig :: Lens (Snaplet a) SnapletConfig
+ Snap.Snaplet: snapletValue :: Lens (Snaplet a) a
+ Snap.Snaplet: subSnaplet :: (Lens a (Snaplet b)) -> (Lens (Snaplet a) (Snaplet b))
+ Snap.Snaplet: with :: MonadSnaplet m => (Lens v (Snaplet v')) -> m b v' a -> m b v a
+ Snap.Snaplet: with' :: MonadSnaplet m => (Lens (Snaplet v) (Snaplet v')) -> m b v' a -> m b v a
+ Snap.Snaplet: withTop :: MonadSnaplet m => (Lens b (Snaplet v')) -> m b v' a -> m b v a
+ Snap.Snaplet: withTop' :: MonadSnaplet m => (Lens (Snaplet b) (Snaplet v')) -> m b v' a -> m b v a
+ Snap.Snaplet: wrapHandlers :: (Handler b v () -> Handler b v ()) -> Initializer b v ()
+ Snap.Snaplet.Auth: AuthError :: String -> AuthFailure
+ Snap.Snaplet.Auth: AuthManager :: r -> Lens b (Snaplet SessionManager) -> Maybe AuthUser -> Int -> ByteString -> Maybe Int -> Key -> Maybe (Int, NominalDiffTime) -> AuthManager b
+ Snap.Snaplet.Auth: AuthSettings :: Int -> ByteString -> Maybe Int -> Maybe (Int, NominalDiffTime) -> FilePath -> AuthSettings
+ Snap.Snaplet.Auth: AuthUser :: Maybe UserId -> Text -> Maybe Password -> Maybe UTCTime -> Maybe UTCTime -> Maybe Text -> Int -> Int -> Maybe UTCTime -> Maybe UTCTime -> Maybe UTCTime -> Maybe ByteString -> Maybe ByteString -> Maybe UTCTime -> Maybe UTCTime -> [Role] -> HashMap Text Value -> AuthUser
+ Snap.Snaplet.Auth: BackendError :: String -> BackendError
+ Snap.Snaplet.Auth: ClearText :: ByteString -> Password
+ Snap.Snaplet.Auth: DuplicateLogin :: BackendError
+ Snap.Snaplet.Auth: Encrypted :: ByteString -> Password
+ Snap.Snaplet.Auth: IncorrectPassword :: AuthFailure
+ Snap.Snaplet.Auth: LockedOut :: UTCTime -> AuthFailure
+ Snap.Snaplet.Auth: PasswordMissing :: AuthFailure
+ Snap.Snaplet.Auth: Role :: ByteString -> Role
+ Snap.Snaplet.Auth: UserId :: Text -> UserId
+ Snap.Snaplet.Auth: UserNotFound :: AuthFailure
+ Snap.Snaplet.Auth: activeUser :: AuthManager b -> Maybe AuthUser
+ Snap.Snaplet.Auth: addAuthSplices :: HasHeist b => Lens b (Snaplet (AuthManager b)) -> Initializer b v ()
+ Snap.Snaplet.Auth: asLockout :: AuthSettings -> Maybe (Int, NominalDiffTime)
+ Snap.Snaplet.Auth: asMinPasswdLen :: AuthSettings -> Int
+ Snap.Snaplet.Auth: asRememberCookieName :: AuthSettings -> ByteString
+ Snap.Snaplet.Auth: asRememberPeriod :: AuthSettings -> Maybe Int
+ Snap.Snaplet.Auth: asSiteKey :: AuthSettings -> FilePath
+ Snap.Snaplet.Auth: authenticatePassword :: AuthUser -> Password -> Maybe AuthFailure
+ Snap.Snaplet.Auth: backend :: AuthManager b -> r
+ Snap.Snaplet.Auth: checkPassword :: Password -> Password -> Bool
+ Snap.Snaplet.Auth: checkPasswordAndLogin :: AuthUser -> Password -> Handler b (AuthManager b) (Either AuthFailure AuthUser)
+ Snap.Snaplet.Auth: class IAuthBackend r
+ Snap.Snaplet.Auth: createUser :: Text -> ByteString -> Handler b (AuthManager b) AuthUser
+ Snap.Snaplet.Auth: currentUser :: Handler b (AuthManager b) (Maybe AuthUser)
+ Snap.Snaplet.Auth: data AuthFailure
+ Snap.Snaplet.Auth: data AuthManager b
+ Snap.Snaplet.Auth: data AuthSettings
+ Snap.Snaplet.Auth: data AuthUser
+ Snap.Snaplet.Auth: data BackendError
+ Snap.Snaplet.Auth: data Password
+ Snap.Snaplet.Auth: data Role
+ Snap.Snaplet.Auth: defAuthSettings :: AuthSettings
+ Snap.Snaplet.Auth: defAuthUser :: AuthUser
+ Snap.Snaplet.Auth: destroy :: IAuthBackend r => r -> AuthUser -> IO ()
+ Snap.Snaplet.Auth: destroyUser :: AuthUser -> Handler b (AuthManager b) ()
+ Snap.Snaplet.Auth: encryptPassword :: Password -> IO Password
+ Snap.Snaplet.Auth: forceLogin :: AuthUser -> Handler b (AuthManager b) (Either AuthFailure AuthUser)
+ Snap.Snaplet.Auth: ifLoggedIn :: Lens b (Snaplet (AuthManager b)) -> SnapletSplice b v
+ Snap.Snaplet.Auth: ifLoggedOut :: Lens b (Snaplet (AuthManager b)) -> SnapletSplice b v
+ Snap.Snaplet.Auth: isLoggedIn :: Handler b (AuthManager b) Bool
+ Snap.Snaplet.Auth: lockout :: AuthManager b -> Maybe (Int, NominalDiffTime)
+ Snap.Snaplet.Auth: loginByRememberToken :: Handler b (AuthManager b) (Maybe AuthUser)
+ Snap.Snaplet.Auth: loginByUsername :: ByteString -> Password -> Bool -> Handler b (AuthManager b) (Either AuthFailure AuthUser)
+ Snap.Snaplet.Auth: loginUser :: ByteString -> ByteString -> Maybe ByteString -> (AuthFailure -> Handler b (AuthManager b) ()) -> Handler b (AuthManager b) () -> Handler b (AuthManager b) ()
+ Snap.Snaplet.Auth: logout :: Handler b (AuthManager b) ()
+ Snap.Snaplet.Auth: logoutUser :: Handler b (AuthManager b) () -> Handler b (AuthManager b) ()
+ Snap.Snaplet.Auth: lookupByLogin :: IAuthBackend r => r -> Text -> IO (Maybe AuthUser)
+ Snap.Snaplet.Auth: lookupByRememberToken :: IAuthBackend r => r -> Text -> IO (Maybe AuthUser)
+ Snap.Snaplet.Auth: lookupByUserId :: IAuthBackend r => r -> UserId -> IO (Maybe AuthUser)
+ Snap.Snaplet.Auth: markAuthFail :: AuthUser -> Handler b (AuthManager b) AuthUser
+ Snap.Snaplet.Auth: markAuthSuccess :: AuthUser -> Handler b (AuthManager b) AuthUser
+ Snap.Snaplet.Auth: minPasswdLen :: AuthManager b -> Int
+ Snap.Snaplet.Auth: newtype UserId
+ Snap.Snaplet.Auth: registerUser :: ByteString -> ByteString -> Handler b (AuthManager b) AuthUser
+ Snap.Snaplet.Auth: rememberCookieName :: AuthManager b -> ByteString
+ Snap.Snaplet.Auth: rememberPeriod :: AuthManager b -> Maybe Int
+ Snap.Snaplet.Auth: requireUser :: Lens b (Snaplet (AuthManager b)) -> Handler b v a -> Handler b v a -> Handler b v a
+ Snap.Snaplet.Auth: save :: IAuthBackend r => r -> AuthUser -> IO AuthUser
+ Snap.Snaplet.Auth: saveUser :: AuthUser -> Handler b (AuthManager b) AuthUser
+ Snap.Snaplet.Auth: session :: AuthManager b -> Lens b (Snaplet SessionManager)
+ Snap.Snaplet.Auth: setPassword :: AuthUser -> ByteString -> IO AuthUser
+ Snap.Snaplet.Auth: siteKey :: AuthManager b -> Key
+ Snap.Snaplet.Auth: unUid :: UserId -> Text
+ Snap.Snaplet.Auth: userActivatedAt :: AuthUser -> Maybe UTCTime
+ Snap.Snaplet.Auth: userCreatedAt :: AuthUser -> Maybe UTCTime
+ Snap.Snaplet.Auth: userCurrentLoginAt :: AuthUser -> Maybe UTCTime
+ Snap.Snaplet.Auth: userCurrentLoginIp :: AuthUser -> Maybe ByteString
+ Snap.Snaplet.Auth: userFailedLoginCount :: AuthUser -> Int
+ Snap.Snaplet.Auth: userId :: AuthUser -> Maybe UserId
+ Snap.Snaplet.Auth: userLastLoginAt :: AuthUser -> Maybe UTCTime
+ Snap.Snaplet.Auth: userLastLoginIp :: AuthUser -> Maybe ByteString
+ Snap.Snaplet.Auth: userLockedOutUntil :: AuthUser -> Maybe UTCTime
+ Snap.Snaplet.Auth: userLogin :: AuthUser -> Text
+ Snap.Snaplet.Auth: userLoginCount :: AuthUser -> Int
+ Snap.Snaplet.Auth: userMeta :: AuthUser -> HashMap Text Value
+ Snap.Snaplet.Auth: userPassword :: AuthUser -> Maybe Password
+ Snap.Snaplet.Auth: userRememberToken :: AuthUser -> Maybe Text
+ Snap.Snaplet.Auth: userRoles :: AuthUser -> [Role]
+ Snap.Snaplet.Auth: userSuspendedAt :: AuthUser -> Maybe UTCTime
+ Snap.Snaplet.Auth: userUpdatedAt :: AuthUser -> Maybe UTCTime
+ Snap.Snaplet.Auth: usernameExists :: Text -> Handler b (AuthManager b) Bool
+ Snap.Snaplet.Auth: withBackend :: (forall r. IAuthBackend r => r -> Handler b (AuthManager v) a) -> Handler b (AuthManager v) a
+ Snap.Snaplet.Auth.Backends.JsonFile: initJsonFileAuthManager :: AuthSettings -> Lens b (Snaplet SessionManager) -> FilePath -> SnapletInit b (AuthManager b)
+ Snap.Snaplet.Auth.Backends.JsonFile: instance FromJSON AuthUser
+ Snap.Snaplet.Auth.Backends.JsonFile: instance FromJSON Password
+ Snap.Snaplet.Auth.Backends.JsonFile: instance FromJSON UserCache
+ Snap.Snaplet.Auth.Backends.JsonFile: instance FromJSON UserIdCache
+ Snap.Snaplet.Auth.Backends.JsonFile: instance IAuthBackend JsonFileAuthManager
+ Snap.Snaplet.Auth.Backends.JsonFile: instance ToJSON AuthUser
+ Snap.Snaplet.Auth.Backends.JsonFile: instance ToJSON Password
+ Snap.Snaplet.Auth.Backends.JsonFile: instance ToJSON UserCache
+ Snap.Snaplet.Auth.Backends.JsonFile: instance ToJSON UserIdCache
+ Snap.Snaplet.Auth.Backends.JsonFile: mkJsonAuthMgr :: FilePath -> IO JsonFileAuthManager
+ Snap.Snaplet.Heist: addSplices :: HasHeist b => [(Text, SnapletSplice b v)] -> Initializer b v ()
+ Snap.Snaplet.Heist: addTemplates :: HasHeist b => ByteString -> Initializer b v ()
+ Snap.Snaplet.Heist: addTemplatesAt :: HasHeist b => ByteString -> FilePath -> Initializer b v ()
+ Snap.Snaplet.Heist: bindSnapletSplices :: (Lens (Snaplet b) (Snaplet v)) -> [(Text, SnapletSplice b v)] -> TemplateState (Handler b b) -> TemplateState (Handler b b)
+ Snap.Snaplet.Heist: class HasHeist b
+ Snap.Snaplet.Heist: clearHeistCache :: Heist b -> IO ()
+ Snap.Snaplet.Heist: data Heist b
+ Snap.Snaplet.Heist: data SnapletHeist b v a
+ Snap.Snaplet.Heist: heistInit :: FilePath -> SnapletInit b (Heist b)
+ Snap.Snaplet.Heist: heistLens :: HasHeist b => Lens (Snaplet b) (Snaplet (Heist b))
+ Snap.Snaplet.Heist: heistLocal :: HasHeist b => (TemplateState (Handler b b) -> TemplateState (Handler b b)) -> Handler b v a -> Handler b v a
+ Snap.Snaplet.Heist: heistServe :: HasHeist b => Handler b v ()
+ Snap.Snaplet.Heist: heistServeSingle :: HasHeist b => ByteString -> Handler b v ()
+ Snap.Snaplet.Heist: liftAppHandler :: Handler b b a -> SnapletHeist b v a
+ Snap.Snaplet.Heist: liftHandler :: Handler b v a -> SnapletHeist b v a
+ Snap.Snaplet.Heist: liftHeist :: HeistT (Handler b b) a -> SnapletHeist b v a
+ Snap.Snaplet.Heist: liftWith :: (Lens (Snaplet b) (Snaplet v')) -> Handler b v' a -> SnapletHeist b v a
+ Snap.Snaplet.Heist: modifyHeistTS :: HasHeist b => (TemplateState (Handler b b) -> TemplateState (Handler b b)) -> Initializer b v ()
+ Snap.Snaplet.Heist: render :: HasHeist b => ByteString -> Handler b v ()
+ Snap.Snaplet.Heist: renderAs :: HasHeist b => ByteString -> ByteString -> Handler b v ()
+ Snap.Snaplet.Heist: renderWithSplices :: HasHeist b => ByteString -> [(Text, SnapletSplice b v)] -> Handler b v ()
+ Snap.Snaplet.Heist: type SnapletSplice b v = SnapletHeist b v Template
+ Snap.Snaplet.Heist: withHeistTS :: HasHeist b => (TemplateState (Handler b b) -> a) -> Handler b v a
+ Snap.Snaplet.Heist: withSplices :: HasHeist b => [(Text, SnapletSplice b v)] -> Handler b v a -> Handler b v a
+ Snap.Snaplet.Session: commitSession :: Handler b SessionManager ()
+ Snap.Snaplet.Session: csrfToken :: Handler b SessionManager Text
+ Snap.Snaplet.Session: data SessionManager
+ Snap.Snaplet.Session: deleteFromSession :: Text -> Handler b SessionManager ()
+ Snap.Snaplet.Session: getFromSession :: Text -> Handler b SessionManager (Maybe Text)
+ Snap.Snaplet.Session: resetSession :: Handler b SessionManager ()
+ Snap.Snaplet.Session: sessionToList :: Handler b SessionManager [(Text, Text)]
+ Snap.Snaplet.Session: setInSession :: Text -> Text -> Handler b SessionManager ()
+ Snap.Snaplet.Session: touchSession :: Handler b SessionManager ()
+ Snap.Snaplet.Session: withSession :: (Lens b (Snaplet SessionManager)) -> Handler b v a -> Handler b v a
+ Snap.Snaplet.Session.Backends.CookieSession: initCookieSessionManager :: FilePath -> ByteString -> Maybe Int -> SnapletInit b SessionManager
+ Snap.Snaplet.Session.Backends.CookieSession: instance (Serialize k, Serialize v, Hashable k, Eq k) => Serialize (HashMap k v)
+ Snap.Snaplet.Session.Backends.CookieSession: instance Eq CookieSession
+ Snap.Snaplet.Session.Backends.CookieSession: instance Eq Payload
+ Snap.Snaplet.Session.Backends.CookieSession: instance ISessionManager CookieSessionManager
+ Snap.Snaplet.Session.Backends.CookieSession: instance Ord Payload
+ Snap.Snaplet.Session.Backends.CookieSession: instance Serialize CookieSession
+ Snap.Snaplet.Session.Backends.CookieSession: instance Serialize Payload
+ Snap.Snaplet.Session.Backends.CookieSession: instance Show CookieSession
+ Snap.Snaplet.Session.Backends.CookieSession: instance Show CookieSessionManager
+ Snap.Snaplet.Session.Backends.CookieSession: instance Show Payload
+ Snap.Snaplet.Session.Backends.CookieSession: instance Typeable CookieSessionManager

Files

CONTRIBUTORS view
@@ -1,8 +1,6 @@ Ozgun Ataman <ozataman@gmail.com> Doug Beardsley <mightybyte@gmail.com> Gregory Collins <greg@gregorycollins.net>-Shu-yu Guo <shu@rfrn.org> Carl Howells <chowells79@gmail.com>-Shane O'Brien <shane@duairc.com>-James Sanders <jimmyjazz14@gmail.com>-Jacob Stanley <jystic@jystic.com>+Chris Smith <cdsmith@gmail.com>+Jurriën Stutterheim <j.stutterheim@me.com>
README.SNAP.md view
@@ -16,11 +16,11 @@    * a sensible and clean monad for web programming -  * an xml-based templating system for generating HTML based on-    [expat](http://expat.sourceforge.net/) (via-    [hexpat](http://hackage.haskell.org/package/hexpat)) that allows you to-    bind Haskell functionality to XML tags without getting PHP-style tag soup-    all over your pants+  * an xml-based templating system for generating HTML that allows you to bind+    Haskell functionality to XML tags without getting PHP-style tag soup all+    over your pants++  * a "snaplet" system for building web sites from composable pieces.  Snap is currently only officially supported on Unix platforms; it has been tested on Linux and Mac OSX Snow Leopard, and is reported to work on Windows.
README.md view
@@ -1,18 +1,14 @@ Snap Framework ============== -This is the first developer prerelease of the Snap Framework snap-tool.  For more information about Snap, read the `README.SNAP.md` or-visit the Snap project website at http://www.snapframework.com/.--Snap is a nascent web framework for Haskell, based on iteratee I/O (as-[popularized by Oleg-Kiselyov](http://okmij.org/ftp/Streams.html#iteratee)).-+Snap is a web framework for Haskell, based on iteratee I/O (as [popularized by+Oleg Kiselyov](http://okmij.org/ftp/Streams.html#iteratee)).  For more+information about Snap, read the `README.SNAP.md` or visit the Snap project+website at http://www.snapframework.com/.  ## Library contents -This is the `snap` executable and supporting library, which contains:+This is top-level project for the Snap Framework, which contains:    * a command-line utility for creating initial Snap applications @@ -20,12 +16,14 @@     fly in development mode, with no performance loss in production     mode. +  * a "snaplet" API allowing web applications to be build from composable+    pieces.  Building snap =============  The snap tool and library are built using- [Cabal](http://www.haskell.org/cabal/) and+[Cabal](http://www.haskell.org/cabal/) and [Hackage](http://hackage.haskell.org/packages/hackage.html). Just run      cabal install@@ -35,20 +33,13 @@  ## Building the Haddock Documentation -The haddock documentation can be built using the supplied `haddock.sh` shell-script:--    ./haddock.sh+The haddock documentation can be built using 'cabal haddock'.  The docs get put in `dist/doc/html/`.   ## Building the testsuite -Snap is still in its very early stages, so most of the "action" (and a big-chunk of the code) right now is centred on the test suite. Snap aims for 100%-test coverage, and we're trying hard to stick to that.- To build the test suite, `cd` into the `test/` directory and run      $ cabal configure@@ -60,3 +51,12 @@   The testsuite generates an `hpc` test coverage report in `test/dist/hpc`.+++## Roadmap to Understanding Snaplets++1. Read Tutorial.lhs which is in `project_template/tutorial/src`.+2. Generate and read the haddock docs.+3. The test code has the nice property that it actually functions as a pretty good example app and covers a lot of the use cases.+4. If you're interested in the implementation, read design.md.+
project_template/barebones/foo.cabal view
@@ -19,8 +19,8 @@     bytestring >= 0.9.1 && < 0.10,     MonadCatchIO-transformers >= 0.2.1 && < 0.3,     mtl >= 2 && < 3,-    snap-core == 0.5.*,-    snap-server == 0.5.*+    snap-core   == 0.6.*,+    snap-server == 0.6.*    if impl(ghc >= 6.12.0)     ghc-options: -threaded -Wall -fwarn-tabs -funbox-strict-fields -O2
project_template/barebones/src/Main.hs view
@@ -2,7 +2,7 @@ module Main where  import           Control.Applicative-import           Snap.Types+import           Snap.Core import           Snap.Util.FileServe import           Snap.Http.Server 
project_template/default/foo.cabal view
@@ -21,21 +21,20 @@   Build-depends:     base >= 4 && < 5,     bytestring >= 0.9.1 && < 0.10,-    heist >= 0.5 && < 0.6,+    data-lens >= 2.0.1 && < 2.1,+    data-lens-template >= 2.1 && < 2.2,+    heist >= 0.6 && < 0.7,     MonadCatchIO-transformers >= 0.2.1 && < 0.3,     mtl >= 2 && < 3,-    snap == 0.5.*,-    snap-core == 0.5.*,-    snap-server == 0.5.*,+    snap == 0.6.*,+    snap-core   == 0.6.*,+    snap-server == 0.6.*,     text >= 0.11 && < 0.12,     time >= 1.1 && < 1.4,     xmlhtml == 0.1.* -  extensions: TypeSynonymInstances MultiParamTypeClasses-   if flag(development)     cpp-options: -DDEVELOPMENT-    build-depends: hint >= 0.3.2 && < 0.4     -- In development mode, speed is already going to suffer, so skip     -- the fancy optimization flags.  Additionally, disable all     -- warnings.  The hint library doesn't give an option to execute
project_template/default/resources/templates/index.tpl view
@@ -1,7 +1,7 @@ <html>   <head>     <title>Snap web server</title>-    <link rel="stylesheet" type="text/css" href="screen.css"/>+    <link rel="stylesheet" type="text/css" href="/screen.css"/>   </head>   <body>     <div id="content">
project_template/default/src/Application.hs view
@@ -1,58 +1,29 @@+{-# LANGUAGE TemplateHaskell #-}+ {- -This module defines our application's monad and any application-specific-information it requires.+This module defines our application's state type and an alias for its handler+monad.  -} -module Application-  ( Application-  , applicationInitializer-  ) where--import           Snap.Extension-import           Snap.Extension.Heist.Impl-import           Snap.Extension.Timer.Impl-+module Application where ---------------------------------------------------------------------------------- | 'Application' is our application's monad. It uses 'SnapExtend' from--- 'Snap.Extension' to provide us with an extended 'MonadSnap' making use of--- the Heist and Timer Snap extensions.-type Application = SnapExtend ApplicationState+import Data.Lens.Template+import Data.Time.Clock +import Snap.Snaplet+import Snap.Snaplet.Heist ---------------------------------------------------------------------------------- | 'ApplicationState' is a record which contains the state needed by the Snap--- extensions we're using.  We're using Heist so we can easily render Heist--- templates, and Timer simply to illustrate the config loading differences--- between development and production modes.-data ApplicationState = ApplicationState-    { templateState :: HeistState Application-    , timerState    :: TimerState+data App = App+    { _heist :: Snaplet (Heist App)+    , _startTime :: UTCTime     } ---------------------------------------------------------------------------------instance HasHeistState Application ApplicationState where-    getHeistState     = templateState-    setHeistState s a = a { templateState = s }-+type AppHandler = Handler App App --------------------------------------------------------------------------------instance HasTimerState ApplicationState where-    getTimerState     = timerState-    setTimerState s a = a { timerState = s }+makeLens ''App +instance HasHeist App where+    heistLens = subSnaplet heist ---------------------------------------------------------------------------------- | The 'Initializer' for ApplicationState. For more on 'Initializer's, see--- the documentation from the snap package. Briefly, this is used to--- generate the 'ApplicationState' needed for our application and will--- automatically generate reload\/cleanup actions for us which we don't need--- to worry about.-applicationInitializer :: Initializer ApplicationState-applicationInitializer = do-    heist <- heistInitializer "resources/templates" id-    timer <- timerInitializer-    return $ ApplicationState heist timer
project_template/default/src/Main.hs view
@@ -1,6 +1,27 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE TemplateHaskell #-} +module Main where++import           Control.Exception (SomeException, try)++import qualified Data.Text as T++import           Snap.Http.Server+import           Snap.Snaplet+import           Snap.Core++import           System.IO++import           Site++#ifdef DEVELOPMENT+import           Snap.Loader.Devel+#else+import           Snap.Loader.Prod+#endif++ {-|  This is the entry point for this web server application.  It supports@@ -38,31 +59,51 @@ change.  -}+main :: IO ()+main = do+    -- depending on the version of loadSnapTH in scope, this either+    -- enables dynamic reloading, or compiles it without.  The last+    -- argument to loadSnapTH is a list of additional directories to+    -- watch for changes to trigger reloads in development mode.  It+    -- doesn't need to include source directories, those are picked up+    -- automatically by the splice.+    (conf, site, cleanup) <- $(loadSnapTH [| getConf |]+                                          'getActions+                                          ["resources/templates"]) -module Main where+    _ <- try $ httpServe conf $ site :: IO (Either SomeException ())+    cleanup -#ifdef DEVELOPMENT-import           Control.Exception (SomeException, try) -import           Snap.Extension.Loader.Devel-import           Snap.Http.Server (quickHttpServe)-#else-import           Snap.Extension.Server-#endif+-- | This action loads the config used by this application.  The+-- loaded config is returned as the first element of the tuple+-- produced by the loadSnapTH Splice.  The type is not solidly fixed,+-- though it must be an IO action that produces the same type as+-- 'getActions' takes.  It also must be an instance of Typeable.  If+-- the type of this is changed, a full recompile will be needed to+-- pick up the change, even in development mode.+--+-- This action is only run once, regardless of whether development or+-- production mode is in use.+getConf :: IO (Config Snap ())+getConf = commandLineConfig defaultConfig -import           Application-import           Site -main :: IO ()-#ifdef DEVELOPMENT-main = do-    -- All source directories will be watched for updates-    -- automatically.  If any extra directories should be watched for-    -- updates, include them here.-    (snap, cleanup) <- $(let watchDirs = ["resources/templates"]-                         in loadSnapTH 'applicationInitializer 'site watchDirs)-    try $ quickHttpServe snap :: IO (Either SomeException ())-    cleanup-#else-main = quickHttpServe applicationInitializer site-#endif+-- | This function generates the the site handler and cleanup action+-- from the configuration.  In production mode, this action is only+-- run once.  In development mode, this action is run whenever the+-- application is reloaded.+--+-- Development mode also makes sure that the cleanup actions are run+-- appropriately before shutdown.  The cleanup action returned from+-- loadSnapTH should still be used after the server has stopped+-- handling requests, as the cleanup actions are only automatically+-- run when a reload is triggered.+--+-- This sample doesn't actually use the config passed in, but more+-- sophisticated code might.+getActions :: Config Snap () -> IO (Snap (), IO ())+getActions _ = do+    (msgs, site, cleanup) <- runSnaplet app+    hPutStrLn stderr $ T.unpack msgs+    return (site, cleanup)
project_template/default/src/Site.hs view
@@ -3,22 +3,29 @@ {-|  This is where all the routes and handlers are defined for your site. The-'site' function combines everything together and is exported by this module.+'app' function is the initializer that combines everything together and+is exported by this module.  -}  module Site-  ( site+  ( app   ) where  import           Control.Applicative+import           Control.Monad.Trans+import           Control.Monad.State+import           Data.ByteString (ByteString) import           Data.Maybe+import qualified Data.Text as T import qualified Data.Text.Encoding as T-import           Snap.Extension.Heist-import           Snap.Extension.Timer+import           Data.Time.Clock+import           Snap.Core+import           Snap.Snaplet+import           Snap.Snaplet.Heist import           Snap.Util.FileServe-import           Snap.Types import           Text.Templating.Heist+import           Text.XmlHtml hiding (render)  import           Application @@ -29,7 +36,7 @@ -- The 'ifTop' is required to limit this to the top of a route. -- Otherwise, the way the route table is currently set up, this action -- would be given every request.-index :: Application ()+index :: Handler App App () index = ifTop $ heistLocal (bindSplices indexSplices) $ render "index"   where     indexSplices =@@ -39,8 +46,24 @@   ------------------------------------------------------------------------------+-- | For your convenience, a splice which shows the start time.+startTimeSplice :: Splice AppHandler+startTimeSplice = do+    time <- lift $ gets _startTime+    return $ [TextNode $ T.pack $ show $ time]+++------------------------------------------------------------------------------+-- | For your convenience, a splice which shows the current time.+currentTimeSplice :: Splice AppHandler+currentTimeSplice = do+    time <- liftIO getCurrentTime+    return $ [TextNode $ T.pack $ show $ time]+++------------------------------------------------------------------------------ -- | Renders the echo page.-echo :: Application ()+echo :: Handler App App () echo = do     message <- decodedParam "stuff"     heistLocal (bindString "message" (T.decodeUtf8 message)) $ render "echo"@@ -49,9 +72,21 @@   --------------------------------------------------------------------------------- | The main entry point handler.-site :: Application ()-site = route [ ("/",            index)-             , ("/echo/:stuff", echo)-             ]-       <|> serveDirectory "resources/static"+-- | The application's routes.+routes :: [(ByteString, Handler App App ())]+routes = [ ("/",            index)+         , ("/echo/:stuff", echo)+         , ("", with heist heistServe)+         , ("", serveDirectory "resources/static")+         ]++------------------------------------------------------------------------------+-- | The application initializer.+app :: SnapletInit App App+app = makeSnaplet "app" "An snaplet example application." Nothing $ do+    sTime <- liftIO getCurrentTime+    h <- nestSnaplet "heist" heist $ heistInit "resources/templates"+    addRoutes routes+    return $ App h sTime++
− project_template/default/src/Snap/Extension/Timer.hs
@@ -1,53 +0,0 @@-{-|--'Snap.Extension.Timer' exports the 'MonadTimer' interface which allows you to-keep track of the time at which your application was started. The interface's-only operation is 'startTime'.--Two splices, 'startTimeSplice' and 'currentTimeSplice' are also provided, for-your convenience.--'Snap.Extension.Timer.Timer' contains the only implementation of this-interface and can be used to turn your application's monad into a-'MonadTimer'.--More than anything else, this is intended to serve as an example Snap-Extension to any developer wishing to write their own Snap Extension.---}--module Snap.Extension.Timer-  ( MonadTimer(..)-  , startTimeSplice-  , currentTimeSplice-  ) where--import           Control.Monad.Trans-import           Data.Time.Clock-import qualified Data.Text as T-import           Snap.Types-import           Text.Templating.Heist-import           Text.XmlHtml------------------------------------------------------------------------------------ | The 'MonadTimer' type class. Minimal complete definition: 'startTime'.-class MonadSnap m => MonadTimer m where-    -- | The time at which your application was last loaded.-    startTime :: m UTCTime------------------------------------------------------------------------------------ | For your convenience, a splice which shows the start time.-startTimeSplice :: MonadTimer m => Splice m-startTimeSplice = do-    time <- lift startTime-    return $ [TextNode $ T.pack $ show $ time]------------------------------------------------------------------------------------ | For your convenience, a splice which shows the current time.-currentTimeSplice :: MonadTimer m => Splice m-currentTimeSplice = do-    time <- lift $ liftIO getCurrentTime-    return $ [TextNode $ T.pack $ show $ time]
− project_template/default/src/Snap/Extension/Timer/Impl.hs
@@ -1,67 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--{-|--'Snap.Extension.Timer.Impl' is an implementation of the 'MonadTimer'-interface defined in 'Snap.Extension.Timer'.--As always, to use, add 'TimerState' to your application's state, along with an-instance of 'HasTimerState' for your application's state, making sure to use a-'timerInitializer' in your application's 'Initializer', and then you're ready to go.--This implementation does not require that your application's monad implement-interfaces from any other Snap Extension.---}--module Snap.Extension.Timer.Impl-  ( TimerState-  , HasTimerState(..)-  , timerInitializer-  , module Snap.Extension.Timer-  ) where--import           Control.Monad.Reader-import           Data.Time.Clock-import           Snap.Extension-import           Snap.Extension.Timer-import           Snap.Types----------------------------------------------------------------------------------- | Your application's state must include a 'TimerState' in order for your--- application to be a 'MonadTimer'.-newtype TimerState = TimerState-    { _startTime :: UTCTime-    }------------------------------------------------------------------------------------ | For your application's monad to be a 'MonadTimer', your application's--- state needs to be an instance of 'HasTimerState'. Minimal complete--- definition: 'getTimerState', 'setTimerState'.-class HasTimerState s where-    getTimerState :: s -> TimerState-    setTimerState :: TimerState -> s -> s------------------------------------------------------------------------------------ | The Initializer for 'TimerState'. No arguments are required.-timerInitializer :: Initializer TimerState-timerInitializer = liftIO getCurrentTime >>= mkInitializer . TimerState----------------------------------------------------------------------------------instance InitializerState TimerState where-    extensionId = const "Timer/Timer"-    mkCleanup   = const $ return ()-    mkReload    = const $ return ()----------------------------------------------------------------------------------instance HasTimerState s => MonadTimer (SnapExtend s) where-    startTime = fmap _startTime $ asks getTimerState----------------------------------------------------------------------------------instance (MonadSnap m, HasTimerState s) => MonadTimer (ReaderT s m) where-    startTime = fmap _startTime $ asks getTimerState
snap.cabal view
@@ -1,13 +1,13 @@ name:           snap-version:        0.5.5.1+version:        0.6.0 synopsis:       Snap: A Haskell Web Framework: project starter executable and glue code library description:    Snap Framework project starter executable and glue code library license:        BSD3 license-file:   LICENSE-author:         James Sanders, Shu-yu Guo, Gregory Collins, Doug Beardsley+author:         Ozgun Ataman, Doug Beardsley, Gregory Collins, Carl Howells, Chris Smith maintainer:     snap@snapframework.com build-type:     Simple-cabal-version:  >= 1.6+cabal-version:  >= 1.8 homepage:       http://snapframework.com/ category:       Web @@ -27,58 +27,106 @@   project_template/default/src/Application.hs,   project_template/default/src/Main.hs,   project_template/default/src/Site.hs,-  project_template/default/src/Snap/Extension/Timer/Impl.hs,-  project_template/default/src/Snap/Extension/Timer.hs,   extra/hscolour.css,   extra/haddock.css,   extra/logo.gif,   test/snap-testsuite.cabal,-  test/runTests.sh,+  test/runTestsAndCoverage.sh,   test/suite/Snap/TestCommon.hs,   test/suite/TestSuite.hs  Flag hint   Description: Support dynamic project reloading via hint-  Default: True+  Default: False  Library   hs-source-dirs: src -  exposed-modules:-    Snap.Extension.Heist,-    Snap.Extension.Heist.Impl,-    Snap.Extension.Server,-    Snap.Extension-   if flag(hint)     exposed-modules:-      Snap.Extension.Loader.Devel+      Snap.Loader.Devel -    other-modules:-      Snap.Extension.Loader.Devel.Evaluator,-      Snap.Extension.Loader.Devel.Signal,-      Snap.Extension.Loader.Devel.TreeWatcher+  exposed-modules:+    Snap,+    Snap.Loader.Prod+    Snap.Snaplet,+    Snap.Snaplet.Heist,+    Snap.Snaplet.Auth,+    Snap.Snaplet.Auth.Backends.JsonFile,+    Snap.Snaplet.Session,+    Snap.Snaplet.Session.Backends.CookieSession +  other-modules:+    Data.RBAC.Checker,+    Data.RBAC.Role,+    Data.RBAC.Types,+    Data.RBAC.Internal.Role,+    Data.RBAC.Internal.RoleMap,+    Data.RBAC.Internal.Rule,+    Data.RBAC.Internal.Types,+    Snap.Loader.Devel.Evaluator,+    Snap.Loader.Devel.Signal,+    Snap.Loader.Devel.TreeWatcher,+    Snap.Snaplet.Auth.AuthManager,+    Snap.Snaplet.Auth.Types,+    Snap.Snaplet.Auth.Handlers,+    Snap.Snaplet.Auth.SpliceHelpers,+    Snap.Snaplet.HeistNoClass,+    Snap.Snaplet.Internal.Initializer,+    Snap.Snaplet.Internal.LensT,+    Snap.Snaplet.Internal.Lensed,+    Snap.Snaplet.Internal.RST,+    Snap.Snaplet.Internal.Types+    Snap.Snaplet.Session.Common,+    Snap.Snaplet.Session.SecureCookie,+    Snap.Snaplet.Session.SessionManager+   build-depends:-    base                      >= 4 && < 5,-    blaze-builder             >= 0.2.1.4 && <0.4,-    bytestring                >= 0.9.1 && < 0.10,-    directory                 >= 1.0 && < 1.2,-    directory-tree            >= 0.10 && < 0.11,-    enumerator                >= 0.4.13.1 && <0.5,-    filepath                  >= 1.1 && <1.3,-    MonadCatchIO-transformers >= 0.2.1 && < 0.3,-    snap-core                 >= 0.5.4 && <0.6,-    heist                     >= 0.5 && < 0.6,-    template-haskell          >= 2.3 && < 2.7,-    time                      >= 1.0 && < 1.4+    Crypto                    >= 4.2     && < 4.3,+    MonadCatchIO-transformers >= 0.2     && < 0.3,+    aeson                     >= 0.3.2   && < 0.4,+    attoparsec                (>= 0.8.0.2 && < 0.9) ||+                              (>= 0.9.1.1 && < 0.10),+    base                      >= 4       && < 5,+    bytestring                >= 0.9.1   && < 0.10,+    cereal                    >= 0.3     && < 0.4,+    clientsession             >= 0.7.2   && < 0.8,+    configurator              >= 0.1     && < 0.2,+    containers                >= 0.3     && < 0.5,+    directory                 >= 1.0     && < 1.2,+    directory-tree            >= 0.10    && < 0.11,+    data-lens                 >= 2.0.1   && < 2.1,+    data-lens-template        >= 2.1     && < 2.2,+    filepath                  >= 1.1     && < 1.3,+    hashable                  >= 1.1     && < 1.2,+    heist                     >= 0.6     && < 0.7,+    logict                    >= 0.4.2   && < 0.6,+    mtl                       >  2.0     && < 2.1,+    mwc-random                >= 0.8     && < 0.11,+    old-time                  >= 1.0     && < 1.1,+    pwstore-fast              >= 2.2     && < 2.3,+    safe                      >= 0.3     && < 0.4,+    snap-core                 >= 0.6     && < 0.7,+    snap-server               >= 0.6     && < 0.7,+    stm                       >= 2.2     && < 2.3,+    syb                       >= 0.1     && < 0.4,+    template-haskell          >= 2.2     && < 2.7,+    text                      >= 0.11    && < 0.12,+    time                      >= 1.1     && < 1.5,+    transformers              >= 0.2     && < 0.3,+    unordered-containers      >= 0.1.4   && < 0.2,+    utf8-string               >= 0.3     && < 0.4,+    vector                    >= 0.7.1   && < 0.10,+    vector-algorithms         >= 0.4     && < 0.6,+    xmlhtml                   >= 0.1     && < 0.2    if flag(hint)     build-depends:       hint                    >= 0.3.3.1 && < 0.4    if !os(windows)-    build-depends:    unix >= 2.2.0.0 && < 2.6+    build-depends:+      unix                    >= 2.2.0.0 && < 2.6    if impl(ghc >= 6.12.0)     ghc-options: -Wall -fwarn-tabs -funbox-strict-fields -O2@@ -94,27 +142,16 @@   other-modules: Snap.StarterTH    build-depends:-    attoparsec       >= 0.8.0.2 && < 0.10,-    base             >= 4   && < 5,-    bytestring,-    bytestring-nums,-    cereal           >= 0.3 && < 0.4,-    containers,-    directory        >= 1.0 && < 1.2,-    directory-tree,-    dlist            >= 0.5 && < 0.6,-    enumerator       >= 0.4.13.1 && <0.5,-    filepath         >= 1.1 && <1.3,-    mtl              >= 2,-    old-locale,-    old-time,-    snap-core        >= 0.5.4 && <0.6,-    snap-server      >= 0.5.4 && <0.6,-    template-haskell >= 2.3 && < 2.7,-    text             >= 0.11 && <0.12,-    time             >= 1.0 && < 1.4,-    unix-compat,-    zlib+    base                >= 4       && < 5,+    bytestring          >= 0.9.1   && < 0.10,+    containers          >= 0.3     && < 0.5,+    directory           >= 1.0     && < 1.2,+    directory-tree      >= 0.10    && < 0.11,+    filepath            >= 1.1     && < 1.3,+    old-time            >= 1.0     && < 1.1,+    snap-server         >= 0.6     && < 0.7,+    template-haskell    >= 2.2     && < 2.7,+    text                >= 0.11    && < 0.12    ghc-prof-options: -prof -auto-all 
+ src/Data/RBAC/Checker.hs view
@@ -0,0 +1,223 @@+{-# LANGUAGE BangPatterns      #-}+{-# LANGUAGE OverloadedStrings #-}++module Data.RBAC.Checker where++import           Control.Monad+import           Control.Monad.Logic+import           Control.Monad.Reader+import           Control.Monad.State.Lazy+import           Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as M+import           Data.Maybe (fromMaybe, isJust)+import           Data.Text (Text)++import           Data.RBAC.Internal.RoleMap (RoleMap)+import qualified Data.RBAC.Internal.RoleMap as RM+import           Data.RBAC.Internal.Types+import           Data.RBAC.Role+++------------------------------------------------------------------------------+type RoleBuilder a = StateT RoleMap RoleMonad a+++------------------------------------------------------------------------------+applyRule :: Role -> Rule -> [Role]+applyRule r (Rule _ f) = f r+++------------------------------------------------------------------------------+applyRuleSet :: Role -> RuleSet -> [Role]+applyRuleSet r (RuleSet m) = f r+  where+    f = fromMaybe (const []) $ M.lookup (_roleName r) m+++------------------------------------------------------------------------------+checkUnseen :: Role -> RoleBuilder ()+checkUnseen role = do+    m <- get+    if isJust $ RM.lookup role m then mzero else return ()+++------------------------------------------------------------------------------+checkSeen :: Role -> RoleBuilder ()+checkSeen = lnot . checkUnseen+++------------------------------------------------------------------------------+markSeen :: Role -> RoleBuilder ()+markSeen role = modify $ RM.insert role+++------------------------------------------------------------------------------+isum :: (MonadLogic m, MonadPlus m) => [m a] -> m a+isum l = case l of+            []     -> mzero+            (x:xs) -> x `interleave` isum xs+++------------------------------------------------------------------------------+-- | Given a set of roles to check, and a set of implication rules describing+-- how a given role inherits from other roles, this function produces a stream+-- of expanded Roles. If a Role is seen twice, expandRoles mzeros.+expandRoles :: [Rule] -> [Role] -> RoleMonad Role+expandRoles rules roles0 = evalStateT (go roles0) RM.empty+  where+    ruleSet = rulesToSet rules++    go roles = isum $ map expandOne roles++    expandOne role = do+        checkUnseen role+        markSeen role+        return role `interleave` go newRoles++      where+        newRoles = applyRuleSet role ruleSet+++------------------------------------------------------------------------------+hasRole :: Role -> RuleChecker ()+hasRole r = RuleChecker $ do+    ch <- ask+    once $ go ch+  where+    go gen = do+        r' <- lift gen+        if r `matches` r' then return () else mzero+++------------------------------------------------------------------------------+missingRole :: Role -> RuleChecker ()+missingRole = lnot . hasRole+++------------------------------------------------------------------------------+hasAllRoles :: [Role] -> RuleChecker ()+hasAllRoles rs = RuleChecker $ do+    ch <- ask+    lift $ once $ go ch $ RM.fromList rs+  where+    go gen !st = do+        mr <- msplit gen+        maybe mzero+              (\(r,gen') -> let st' = RM.delete r st+                            in if RM.null st'+                                 then return ()+                                 else go gen' st')+              mr+++------------------------------------------------------------------------------+hasAnyRoles :: [Role] -> RuleChecker ()+hasAnyRoles rs = RuleChecker $ do+    ch <- ask+    lift $ once $ go ch+  where+    st = RM.fromList rs+    go gen = do+        mr <- msplit gen+        maybe mzero+              (\(r,gen') -> if isJust $ RM.lookup r st+                                 then return ()+                                 else go gen')+              mr+++------------------------------------------------------------------------------+runRuleChecker :: [Rule]+               -> [Role]+               -> RuleChecker a+               -> Bool+runRuleChecker rules roles (RuleChecker f) =+    case outs of+      []    -> False+      _     -> True+  where+    (RoleMonad st) = runReaderT f $ expandRoles rules roles+    outs = observeMany 1 st+++------------------------------------------------------------------------------+mkRule :: Text -> (Role -> [Role]) -> Rule+mkRule = Rule+++------------------------------------------------------------------------------+implies :: Role -> [Role] -> Rule+implies src dest = Rule (_roleName src)+                        (\role -> if role `matches` src then dest else [])+++------------------------------------------------------------------------------+impliesWith :: Role -> (HashMap Text RoleValue -> [Role]) -> Rule+impliesWith src f = Rule (_roleName src)+                         (\role -> if src `matches` role+                                     then f $ _roleData role+                                     else [])+++------------------------------------------------------------------------------+-- Testing code follows: TODO: move into test suite+++testRules :: [Rule]+testRules = [ "user" `implies` ["guest", "can_post"]+            , "superuser" `implies` [ "user"+                                    , "can_moderate"+                                    , "can_administrate"]+            , "superuser" `implies` [ addRoleData "arg" "*" "with_arg" ]+            , "with_arg" `impliesWith` \dat ->+                maybe [] (\arg -> [addRoleData "arg" arg "dependent_arg"]) $+                      M.lookup "arg" dat+            , "superuser" `implies` [ addRoleData "arg1" "a" $+                                      addRoleData "arg2" "b" "multi_args" ]+            ]++tX :: RuleChecker () -> Bool+tX f = runRuleChecker testRules ["superuser"] f++t1,t2,t3,t4,t5,t6,t7,t8,t9,t10,t11,t12,t13,t14,t15,t16,t17 :: Bool+t1 = tX $ hasAnyRoles ["guest","userz"]++t2 = tX $ hasAllRoles ["guest","userz"]++t3 = tX $ hasAllRoles ["guest","user"]++t4 = tX $ hasRole "can_administrate"++t5 = tX $ hasRole "lkfdhjkjfhds"++t6 = tX $ do+         hasRole "guest"+         hasRole "superuser"++t7 = tX $ do+         hasRole "zzzzz"+         hasRole "superuser"++t8 = tX $ hasRole $ addRoleData "arg" "*" "dependent_arg"++t9 = tX $ hasRole "multi_args"++t10 = tX $ hasRole $ addRoleData "arg2" "b" "multi_args"++t11 = tX $ hasRole $ addRoleData "arg2" "z" "multi_args"++t12 = tX $ hasAllRoles [addRoleData "arg2" "b" "multi_args"]++t13 = tX $ hasAnyRoles [ addRoleData "arg2" "z" "multi_args"+                       , addRoleData "arg2" "b" "multi_args" ]++t14 = tX $ hasAnyRoles [ addRoleData "arg2" "z" "multi_args"+                       , addRoleData "arg2" "aaa" "multi_args" ]++t15 = tX $ missingRole "jflsdkjf"++t16 = tX $ do+          missingRole "fdjlksjlf"+          hasRole "multi_args"++t17 = tX $ missingRole "multi_args"
+ src/Data/RBAC/Internal/Role.hs view
@@ -0,0 +1,81 @@+module Data.RBAC.Internal.Role where++import           Control.Monad.ST+import           Data.Hashable+import           Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as M+import           Data.String+import           Data.Text (Text)+import qualified Data.Vector as V+import qualified Data.Vector.Algorithms.Merge as VA+++------------------------------------------------------------------------------+data RoleValue = RoleBool Bool+               | RoleText Text+               | RoleInt Int+               | RoleDouble Double+  deriving (Ord, Eq, Show)+++instance IsString RoleValue where+    fromString = RoleText . fromString+++instance Hashable RoleValue where+    hashWithSalt salt (RoleBool e)   = hashWithSalt salt e `combine` 7+    hashWithSalt salt (RoleText t)   = hashWithSalt salt t `combine` 196613+    hashWithSalt salt (RoleInt i)    = hashWithSalt salt i `combine` 12582917+    hashWithSalt salt (RoleDouble d) =+        hashWithSalt salt d `combine` 1610612741+++------------------------------------------------------------------------------+data Role = Role {+      _roleName :: Text+    , _roleData :: HashMap Text RoleValue+    }+  deriving (Eq, Show)+++instance IsString Role where+    fromString s = Role (fromString s) M.empty+++------------------------------------------------------------------------------+toSortedList :: (Ord k, Ord v) => HashMap k v -> [(k,v)]+toSortedList m = runST $ do+    v <- V.unsafeThaw $ V.fromList $ M.toList m+    VA.sort v+    v' <- V.unsafeFreeze v+    return $ V.toList v'++++instance Hashable Role where+    hashWithSalt salt (Role nm dat) =+        h $ hashWithSalt salt nm+      where+        h s = hashWithSalt s $ toSortedList dat+++------------------------------------------------------------------------------+data RoleValueMeta = RoleBoolMeta+                   | RoleTextMeta+                   | RoleEnumMeta [Text]+                   | RoleIntMeta+                   | RoleDoubleMeta+++data RoleDataDefinition = RoleDataDefinition {+      _roleDataName        :: Text+    , _roleValueMeta       :: RoleValueMeta+    , _roleDataDescription :: Text+    }+++data RoleMetadata = RoleMetadata {+      _roleMetadataName :: Text+    , _roleDescription  :: Text+    , _roleDataDefs     :: [RoleDataDefinition]+    }
+ src/Data/RBAC/Internal/RoleMap.hs view
@@ -0,0 +1,58 @@+module Data.RBAC.Internal.RoleMap where++import           Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as M+import           Data.HashSet (HashSet)+import qualified Data.HashSet as S+import           Data.List (find, foldl')+import           Data.Text (Text)++import           Data.RBAC.Role+import           Data.RBAC.Internal.Types+++newtype RoleMap = RoleMap (HashMap Text (HashSet Role))+++------------------------------------------------------------------------------+fromList :: [Role] -> RoleMap+fromList = RoleMap . foldl' ins M.empty+  where+    ins m role =+        M.insertWith S.union (_roleName role) (S.singleton role) m+++------------------------------------------------------------------------------+lookup :: Role -> RoleMap -> Maybe Role+lookup role (RoleMap m) = find (`matches` role) l+  where+    l = maybe [] S.toList $ M.lookup (_roleName role) m+++------------------------------------------------------------------------------+delete :: Role -> RoleMap -> RoleMap+delete role (RoleMap m) = RoleMap $ maybe m upd $ M.lookup rNm m+  where+    rNm = _roleName role+    upd s = maybe m+                  (\r -> let s' = S.delete r s+                         in if S.null s'+                              then M.delete rNm m+                              else M.insert rNm s' m)+                  (find (`matches` role) $ S.toList s)+++------------------------------------------------------------------------------+insert :: Role -> RoleMap -> RoleMap+insert role (RoleMap m) =+    RoleMap $ M.insertWith S.union (_roleName role) (S.singleton role) m+++------------------------------------------------------------------------------+empty :: RoleMap+empty = RoleMap M.empty+++------------------------------------------------------------------------------+null :: RoleMap -> Bool+null (RoleMap m) = M.null m
+ src/Data/RBAC/Internal/Rule.hs view
@@ -0,0 +1,31 @@+module Data.RBAC.Internal.Rule where++import           Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as M+import           Data.List (foldl')+import           Data.Monoid+import           Data.Text (Text)++import           Data.RBAC.Internal.Role++------------------------------------------------------------------------------+data Rule = Rule Text (Role -> [Role])++newtype RuleSet = RuleSet (HashMap Text (Role -> [Role]))++instance Monoid RuleSet where+    mempty = RuleSet M.empty+    (RuleSet m1) `mappend` (RuleSet m2) = RuleSet $ M.foldlWithKey' ins m2 m1+      where+        combine f1 f2 r = f1 r ++ f2 r+        ins m k v       = M.insertWith combine k v m+++------------------------------------------------------------------------------+ruleToSet :: Rule -> RuleSet+ruleToSet (Rule nm f) = RuleSet $ M.singleton nm f+++------------------------------------------------------------------------------+rulesToSet :: [Rule] -> RuleSet+rulesToSet = foldl' mappend (RuleSet M.empty) . map ruleToSet
+ src/Data/RBAC/Internal/Types.hs view
@@ -0,0 +1,33 @@+{-# LANGUAGE BangPatterns               #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings          #-}++module Data.RBAC.Internal.Types+  ( module Data.RBAC.Internal.Role+  , module Data.RBAC.Internal.Rule+  , RoleMonad(..)+  , RuleChecker(..)+  ) where++import           Control.Applicative+import           Control.Monad.Reader+import           Control.Monad.Logic++import           Data.RBAC.Internal.Role+import           Data.RBAC.Internal.Rule+++------------------------------------------------------------------------------+-- TODO: should the monads be transformers here? If they were, you could check+-- more complex predicates here+++------------------------------------------------------------------------------+newtype RoleMonad a = RoleMonad { _unRC :: Logic a }+  deriving (Alternative, Applicative, Functor, Monad, MonadPlus, MonadLogic)+++------------------------------------------------------------------------------+newtype RuleChecker a = RuleChecker (ReaderT (RoleMonad Role) RoleMonad a)+  deriving (Alternative, Applicative, Functor, Monad, MonadPlus, MonadLogic)+
+ src/Data/RBAC/Role.hs view
@@ -0,0 +1,24 @@+module Data.RBAC.Role where++import qualified Data.HashMap.Strict as M+import           Data.RBAC.Internal.Types+import           Data.Text (Text)+++------------------------------------------------------------------------------+matches :: Role -> Role -> Bool+matches (Role a1 d1) (Role a2 d2) =+    a1 == a2 && dmatch (toSortedList d1) (toSortedList d2)+  where+    dmatch []         _      = True+    dmatch _          []     = False+    dmatch dds@(d:ds) (e:es) =+        case compare d e of+          LT -> False+          EQ -> dmatch ds es+          GT -> dmatch dds es+++------------------------------------------------------------------------------+addRoleData :: Text -> RoleValue -> Role -> Role+addRoleData k v (Role n d) = Role n $ M.insert k v d
+ src/Data/RBAC/Types.hs view
@@ -0,0 +1,14 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings          #-}++module Data.RBAC.Types+  ( Role(..)                    -- fixme: remove (..)+  , RoleValue(..)               -- fixme+  , RoleValueMeta(..)+  , RoleDataDefinition(..)+  , RoleMetadata(..)+  , Rule+  , RuleChecker+  ) where++import Data.RBAC.Internal.Types
+ src/Snap.hs view
@@ -0,0 +1,26 @@+{-|++This module provides convenience exports of the modules most commonly used+when developing with the Snap Framework.  For documentation about Snaplets,+see "Snap.Snaplet".  For the core web server API, see "Snap.Core".++-}++module Snap+  ( module Control.Applicative+  , module Control.Monad.State+  , module Data.Lens.Common+  , module Data.Lens.Template+  , module Snap.Core+  , module Snap.Http.Server+  , module Snap.Snaplet+  ) where++import Control.Applicative+import Control.Monad.State+import Data.Lens.Common+import Data.Lens.Template+import Snap.Core+import Snap.Http.Server+import Snap.Snaplet+
− src/Snap/Extension.hs
@@ -1,500 +0,0 @@-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE OverloadedStrings #-}--module Snap.Extension-  ( -- * Introduction-    -- $introduction--    -- ** Using Snap Extensions-    -- $using--    -- *** Define Application State and Monad-    -- $definingtypes--    -- *** Provide Instances For \"HasState\" Classes-    -- $hasstateclasses--    -- *** Define The Initializer-    -- $initializer--    -- *** Simplified Snap Extension Server-    -- $httpserve--    SnapExtend-  , Initializer-  , InitializerState(..)-  , runInitializer-  , runInitializerWithReloadAction-  , runInitializerWithoutReloadAction-  , mkInitializer-  , defaultReloadHandler-  , nullReloadHandler-  ) where--import           Blaze.ByteString.Builder-import           Control.Applicative-import           Control.Exception (SomeException)-import           Control.Monad-import           Control.Monad.CatchIO-import           Control.Monad.Reader-import           Data.ByteString (ByteString)-import qualified Data.ByteString.Char8 as B-import           Data.Monoid-import qualified Data.Text as T-import qualified Data.Text.Encoding as T-import           Prelude hiding (catch, init)-import           Snap.Iteratee (enumBuilder, (>==>))-import           Snap.Types-import           System.IO---{- $introduction--  Snap Extensions is a library which makes it easy to create reusable plugins-  that extend your Snap application with modular chunks of functionality such-  as session management, user authentication, templating, or database-  connection pooling.--  We achieve this by requiring that you create a datatype that holds an-  environment for your application and wrap it around the Snap monad. This new-  construct becomes your application's handler monad and gives you access to-  your application state throughout your handlers.--  Warning: this interface is still EXPERIMENTAL and has a very high likelihood-  of changing substantially in coming versions of Snap.---}--{- $using--  Every extension has an interface and at least one implementation of that-  interface.--  For some extensions, like Heist, there is only ever going to be one-  implementation of the interface. In these cases, both the interface and the-  implementation are exported from the same module, Snap.Extension.Heist.Impl.--  Hypothetically, for something like session management though, there could be-  multiple implementations, one using a HDBC backend, one using a MongoDB-  backend and one just using an encrypted cookie as backend. In these cases,-  the interface is exported from Snap.Extension.Session, and the-  implementations live in Snap.Extension.Session.HDBC,-  Snap.Extension.Session.MongoDB and Snap.Extension.Session.CookieStore.--  Keeping this in mind, there are a number of things you need to do to use-  Snap extensions in your application. Let's walk through how to set up a-  simple application with the Heist extension turned on.---}--{- $definingtypes--  First, we define a record type AppState for holding our application's state,-  including the state needed by the extensions we're using.--  At the same time, we also define the monad for our application, App, as-  a type alias to @SnapExtend AppState@. 'SnapExtend' is a 'MonadSnap' and-  a 'MonadReader', whose environment is a given type; in our case, AppState.--  @-module App where--import Database.HDBC-import Database.HDBC.ODBC-import Snap.Extension-import Snap.Extension.Heist-import Snap.Types--type App = SnapExtend AppState--data AppState = AppState-    { heistState  :: HeistState App }-  @--  An important thing to note is that the -State types that we use in the-  fields of AppState are specific to each implementation of a extension's-  interface.  That is, Snap.Extension.Session.HDBC will export a different-  SessionState to Snap.Extension.Session.CookieStore, whose internal-  representation might be completely different.--  This state is what the extension's implementation needs to be able to do its-  job.---}--{- $hasstateclasses--  Now we have a datatype that contains all the internal state needed by our-  application and the extensions it uses. That's a great start! But when do we-  actually get to use this interface and all the functionality that these-  extensions export?  What is actually being extended?--  We use the interface provided by an extension inside our application's-  monad, App. Snap extensions extend our App with new functionality by-  allowing us to user their exported functions inside of our handlers. For-  example, the Heist extension provides the function:--  @render :: MonadHeist m => ByteString -> m ()@ that renders a template by-  its name.--  Is App a 'MonadHeist'? Well, not quite yet. Any 'MonadReader' which is also-  a 'MonadSnap' whose environment contains a 'HeistState' is a 'MonadHeist'.-  That sounds a lot like our App, doesn't it? We just have to tell the Heist-  extension how to find the 'HeistState' in our AppState:--  @-instance HasHeistState AppState where-    getHeistState = heistState-    setHeistState hs as = as { heistState = hs }-  @--  Stated another way, if we give our AppState the ability to hold a HeistState-  and let the HasHeistState typeclass know how to get/set this state, we are-  /automagically/ given the ability to render heist templates in our handlers.--  With these instances, our application's monad App is now a MonadHeist-  giving it access to operations like:--  @render :: MonadHeist m => ByteString -> m ()@--  and--  @heistLocal :: (TemplateState n -> TemplateState n) -> m a -> m a@---}--{- $initializer--  So, our monad is now a 'MonadHeist', but how do we actually construct our-  AppState and turn an App () into a 'Snap' ()? We need to do this upfront,-  once and right before our web server starts listening for connections.--  Snap extensions have a thing called an 'Initializer' that does these things.-  Each implementation of a Snap extension interface provides an 'Initializer'-  for its -State type. We must construct an initializer type for our -State-  type, AppState. An 'Initializer' monad is provided in this library to make-  it easy to do this. For your convenience, 'Initializer' is an instance of-  'MonadIO'.--  @-appInitializer :: Initializer AppState-appInitializer = do-    hs <- heistInitializer \"resources/templates\"-    return $ AppState hs-  @--  In addition to constructing the AppState, the Initializer monad also-  constructs the init, destroy and reload functions for our application from-  the init, reload and destroy functions for the extensions.--  Although it won't cause a compile-time error, it is important to get the-  order of the initializers correct as much as possible, otherwise they may be-  reloaded and destroyed in the wrong order. The "right" order is an order-  where every extension's dependencies are initialised before that extension.-  For example, Snap.Extension.Session.HDBC would depend on something which-  would extend the monad with MonadConnectionPool, i.e.,-  Snap.Extension.ConnectionPool. If you had this configuration it would be-  important that you put the connectionPoolInitializer before the-  sessionInitializer in your appInitializer.--  This Initializer AppState can then be passed to 'runInitializer', which-  combines our initializer action with our application's handler to produce a-  'Snap' handler (which can be passed to 'httpServe'), a cleanup action (which-  you can run after 'httpServe' finishes), and a reload action (which, for-  example, you may want to use in your handler for the path \"admin/reload\".--  The following is an example of how you might use this in main:--  @-main :: IO ()-main = do-    (snap,cleanup,reload) <- runInitializer appInitializer appSite-    let site = snap-               <|> path "admin/reload" $ defaultReloadHandler reload cleanup-    quickHttpServe site `finally` cleanup-  @--  You'll notice we're using 'defaultReloadHandler'. This is a function-  exported by "Snap.Extension" with the type signature--  @MonadSnap m => IO [(ByteString, Maybe ByteString)] -> m ()@ It takes the-  reload action returned by 'runInitializer' and returns a 'Snap' action which-  renders a simple page showing how the reload went. To avoid denial of-  service attacks, the reload handler only works for requests made from the-  local host.---}--{- $httpserve-- This is, of course, a lot of avoidable boilerplate. Snap extensions framework- comes with another module "Snap.Extension.Server", which provides an- interface mimicking that of "Snap.Http.Server". Their function names clash,- so if you need to use both of them in the same module, use a qualified- import. Using this module, the example above becomes:--  @-import Snap.Extension.Server--main :: IO ()-main = quickHttpServe appRunner site-  @--  All it needs is a Initializer AppState and an App () and it is ready to go.-  You might be wondering what happened to all the reload handler bits we-  had before: That stuff has been absorbed into the config for the server.--  One quick note: 'quickHttpServe' doesn't take a config, instead it uses the-  defaults augmented with any options specified on the command-line.  The-  default reload handler path in this case is "admin/reload".--  If you wanted to change this to nullReloadHandler, you would do this:--  @-import Snap.Extension.Server--main :: IO ()-main = do-    config <- commandLineConfig emptyConfig-    httpServe (setReloadHandler nullReloadHandler config) appRunner site-  @--  This behaves exactly as the above example apart from the reload handler.--  With this, we now have a fully functional base application that makes use of-  the Snap Extensions mechanism.--  To initialize a directory with all of this setup provided as a starting-  point, simply @cd@ into the desired location and type: @snap init@. An-  example \"Timer\" extension will also be included for your convenience.---}----------------------------------------------------------------------------------- | A 'SnapExtend' is a 'MonadReader' and a 'MonadSnap' whose environment is--- the application state for a given progam. You would usually type alias--- @SnapExtend AppState@ to something like @App@ to form the monad in which--- you write your application.-newtype SnapExtend s a = SnapExtend (ReaderT s Snap a)-  deriving-    ( Functor-    , Applicative-    , Alternative-    , Monad-    , MonadPlus-    , MonadIO-    , MonadCatchIO-    , MonadSnap-    , MonadReader s-    )------------------------------------------------------------------------------------ | The 'SCR' datatype is used internally by the 'Initializer' monad to store--- the application's state, cleanup actions and reload actions.-data SCR s = SCR-    { _state   :: s-      -- ^ The internal state of the application's Snap Extensions.-    , _cleanup :: IO ()-      -- ^ IO action which when run will cleanup the application's state,-      -- e.g., closing open connections.-    , _reload  :: IO [(ByteString, Maybe ByteString)]-      -- ^ IO action which when run will reload the application's state, e.g.,-      -- refreshing any cached values stored in the state.-      ---      -- It returns a list of tuples whose \"keys\" are the names of the Snap-      -- Extensions which were reloaded and whose \"values\" are @Nothing@-      -- when run successfully and @Just x@ on failure, where @x@ is an error-      -- message.-    }------------------------------------------------------------------------------------ | The 'Initializer' monad. The code that initialises your application's--- state is written in the 'Initializer' monad. It's used for constructing--- values which have cleanup\/destroy and reload actions associated with them.-newtype Initializer s = Initializer (Bool -> IO (Either s (SCR s)))------------------------------------------------------------------------------------ | Values of types which are instances of 'InitializerState' have--- cleanup\/destroy and reload actions associated with them.-class InitializerState s where-    extensionId :: s -> ByteString-    mkCleanup   :: s -> IO ()-    mkReload    :: s -> IO ()------------------------------------------------------------------------------------ | Although it has the same type signature, this is not the same as 'return'--- in the 'Initializer' monad. Return simply lifts a value into the--- 'Initializer' monad, but this lifts the value and its destroy\/reload--- actions. Use this when making your own 'Initializer' actions.-mkInitializer :: InitializerState s => s -> Initializer s-mkInitializer s = Initializer $ \v -> setup v $ Right $ mkSCR v-  where-    handler          :: SomeException -> IO (Maybe ByteString)-    handler e        = return $ Just $ toUTF8 $ show e-    maybeCatch m     = (m >> return Nothing) `catch` handler-    maybeToMsg       = maybe " done." $ const " failed."-    name             = fromUTF8 $ extensionId s-    mkSCR v          = SCR s (cleanup v) (reload v)-    cleanup v        = do-        when v $ hPutStr stderr $ "Cleaning up " ++ name ++ "..."-        m <- maybeCatch $ mkCleanup s-        when v $ hPutStrLn stderr $ maybeToMsg m-    reload v         = do-        when v $ hPutStr stderr $ "Reloading " ++ name ++ "..."-        m <- maybeCatch $ mkReload s-        when v $ hPutStrLn stderr $ maybeToMsg m-        return [(extensionId s, m)]-    setup v r        = do-        when v $ hPutStrLn stderr $ "Initializing " ++ name ++ "... done."-        return r------------------------------------------------------------------------------------ | Given the Initializer for your application's state, and a value in the--- monad formed by 'SnapExtend' wrapped it, this returns a 'Snap' action, a--- cleanup action and a reload action.-runInitializer-  :: Bool-    -- ^ Verbosity; info is printed to 'stderr' when this is 'True'-  -> Initializer s-    -- ^ The Initializer value-  -> SnapExtend s ()-    -- ^ A web handler in your application's monad-  -> IO (Snap (), IO (), IO [(ByteString, Maybe ByteString)])-     -- ^ Returns a 'Snap' handler, a cleanup action, and a reload action. The-     -- list returned by the reload action is for error reporting. There is-     -- one tuple in the list for each Snap extension; the first element of-     -- the tuple is the name of the Snap extension, and the second is a Maybe-     -- which contains Nothing if there was no error reloading that extension-     -- and a Just with the ByteString containing the error message if there-     -- was.-runInitializer v (Initializer r) (SnapExtend m) =-    r v >>= \e -> case e of-        Left s            -> return (runReaderT m s, return (), return [])-        Right (SCR s a b) -> return (runReaderT m s, a, b)------------------------------------------------------------------------------------ | Serves the same purpose as 'runInitializer', but combines the---   application's web handler with a user-supplied action to be run to reload---   the application's state.-runInitializerWithReloadAction-  :: Bool-    -- ^ Verbosity; info is printed to 'stderr' when this is 'True'-  -> Initializer s-    -- ^ The Initializer value-  -> SnapExtend s ()-    -- ^ A web handler in your application's monad.-  -> (IO [(ByteString, Maybe ByteString)] -> SnapExtend s ())-    -- ^ Your desired \"reload\" handler; it gets passed the reload-    -- action. This handler is always run, so you have to guard the path-    -- yourself (with.-  -> IO (Snap (), IO ())-    -- ^ Your 'Snap' handler and a cleanup action.-runInitializerWithReloadAction v (Initializer r) se f = do-    (state, cleanup, reload) <- runInit--    let (SnapExtend m') = f reload <|> se-    return (runReaderT m' state, cleanup)--  where-    runInit = r v >>= \e ->-              case e of-                Left s            -> return (s, return (), return [])-                Right (SCR s a b) -> return (s, a, b)----------------------------------------------------------------------------------- | A cut-down version of 'runInitializer', for use by the hint--- loading code-runInitializerWithoutReloadAction :: Initializer s-                                  -- ^ The Initializer value-                                  -> SnapExtend s ()-                                  -- ^ An action in your application's monad.-                                  -> IO (Snap (), IO ())-runInitializerWithoutReloadAction i se = do-    (action, cleanup, _) <- runInitializer True i se-    return (action, cleanup)----------------------------------------------------------------------------------instance Functor Initializer where-    fmap f (Initializer r) = Initializer $ \v -> r v >>= \e -> return $-        case e of-            Left s            -> Left $ f s-            Right (SCR s a b) -> Right $ SCR (f s) a b----------------------------------------------------------------------------------instance Applicative Initializer where-    pure  = return-    (<*>) = ap----------------------------------------------------------------------------------instance Monad Initializer where-    return   = Initializer . const . return . Left-    a >>= f  = join' $ fmap f a----------------------------------------------------------------------------------instance MonadIO Initializer where-    liftIO = Initializer . const . fmap Left------------------------------------------------------------------------------------ | Join for the 'Initializer' monad. This is used in the definition of bind--- for the 'Initializer' monad.-join' :: Initializer (Initializer s) -> Initializer s-join' (Initializer r) = Initializer $ \v -> r v >>= \e -> case e of-    Left  (Initializer r')           -> r' v-    Right (SCR (Initializer r') a b) -> r' v >>= \e' -> return $ Right $-        case e' of-            Left  s             -> SCR s a b-            Right (SCR s a' b') -> SCR s (a' >> a) (liftM2 (++) b b')------------------------------------------------------------------------------------ | This takes the last value of the tuple returned by 'runInitializer',--- which is a list representing the results of an attempt to reload the--- application's Snap Extensions, and turns it into a Snap action which--- displays the these results.-defaultReloadHandler :: MonadSnap m-                     => IO [(ByteString, Maybe ByteString)]-                     -> m ()-defaultReloadHandler ioms = failIfNotLocal $ do-    ms <- liftIO $ ioms-    let showE e       = mappend "Error: "  $ toUTF8 $ show e-        format (n, m) = mconcat [n, ": ", maybe "Success" showE m, "\n"]-        msg           = mconcat $ map format ms-    finishWith $ setContentType "text/plain; charset=utf-8"-        $ setContentLength (fromIntegral $ B.length msg)-        $ modifyResponseBody (>==> enumBuilder (fromByteString msg))-                             emptyResponse-  where-    failIfNotLocal m = do-        rip <- liftM rqRemoteAddr getRequest-        if not $ elem rip [ "127.0.0.1"-                          , "localhost"-                          , "::1" ]-          then pass-          else m----------------------------------------------------------------------------------- | Use this reload handler to disable the ability to have a web handler--- which reloads Snap extensions.-nullReloadHandler :: MonadSnap m-                  => IO [(ByteString, Maybe ByteString)]-                  -> m ()-nullReloadHandler = const pass----------------------------------------------------------------------------------fromUTF8 :: ByteString -> String-fromUTF8 = T.unpack . T.decodeUtf8--toUTF8 :: String -> ByteString-toUTF8 = T.encodeUtf8 . T.pack
− src/Snap/Extension/Heist.hs
@@ -1,79 +0,0 @@-{-# LANGUAGE OverloadedStrings      #-}-{-# LANGUAGE FunctionalDependencies #-}-{-# LANGUAGE MultiParamTypeClasses  #-}--{-|--'Snap.Extension.Heist' exports the 'MonadHeist' interface which allows you to-integrate Heist templates into your Snap application. The interface's-operations are 'heistServe', 'heistServeSingle', 'heistLocal' and 'render'. As-a convenience, we also provide 'renderWithSplices' that combines 'heistLocal'-and 'render' into a single function call.--'Snap.Extension.Heist.Impl' contains the only implementation of this interface-and can be used to turn your application's monad into a 'MonadHeist'.--'MonadHeist' is unusual among Snap extensions in that it's a multi-parameter-typeclass. The last parameter is your application's monad, and the first is-the monad you want the 'TemplateState' to use. This is usually, but not-always, also your application's monad.--This module should not be used directly. Instead, import-"Snap.Extension.Heist.Impl" in your application.---}--module Snap.Extension.Heist-  ( MonadHeist(..)-  , renderWithSplices ) where--import           Control.Applicative-import           Data.ByteString (ByteString)-import qualified Data.ByteString.Char8 as B-import           Data.Text (Text)-import           Snap.Types-import           Snap.Util.FileServe-import           Text.Templating.Heist------------------------------------------------------------------------------------ | The 'MonadHeist' type class. Minimal complete definition: 'render',--- 'heistLocal'.-class (Monad n, MonadSnap m) => MonadHeist n m | m -> n where-    -- | Renders a template as text\/html. If the given template is not found,-    -- this returns 'empty'.-    render     :: ByteString -> m ()--    -- | Renders a template as the given content type.  If the given template-    -- is not found, this returns 'empty'.-    renderAs   :: ByteString -> ByteString -> m ()--    -- | Runs an action with a modified 'TemplateState'. You might want to use-    -- this if you had a set of splices which were customised for a specific-    -- action. To do that you would do:-    ---    -- > heistLocal (bindSplices mySplices) $ render "myTemplate"-    heistLocal :: (TemplateState n -> TemplateState n) -> m a -> m a--    -- | Analogous to 'fileServe'. If the template specified in the request-    -- path is not found, it returns 'empty'.-    heistServe :: m ()-    heistServe = ifTop (render "index") <|> (render . B.pack =<< getSafePath)--    -- | Analogous to 'fileServeSingle'. If the given template is not found,-    -- this throws an error.-    heistServeSingle :: ByteString -> m ()-    heistServeSingle t = render t-        <|> error ("Template " ++ show t ++ " not found.")------------------------------------------------------------------------------------ | Helper function for common use case:--- Render a template with a given set of splices.-renderWithSplices-  :: (MonadHeist n m)-  => ByteString                 -- ^ Template to render-  -> [(Text, Splice n)]   -- ^ Splice mapping-  -> m ()-renderWithSplices t sps = heistLocal bsps $ render t-  where bsps = bindSplices sps
− src/Snap/Extension/Heist/Impl.hs
@@ -1,219 +0,0 @@-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE FunctionalDependencies #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE OverloadedStrings #-}--{-|--'Snap.Extension.Heist.Impl' is an implementation of the 'MonadHeist'-interface defined in 'Snap.Extension.Heist'.--As always, to use, add 'HeistState' to your application's state, along with an-instance of 'HasHeistState' for your application's state, making sure to-use a 'heistInitializer' in your application's 'Initializer', and then you're-ready to go.--'Snap.Extension.Heist.Impl' is a little different to other Snap Extensions,-which is unfortunate as it is probably the most widely useful one. As-explained below, 'HeistState' takes your application's monad as a type-argument, and 'HasHeistState' is a multi-parameter type class, the additional-first parameter also being your application's monad.--Two instances of 'MonadHeist' are provided with this module. One is designed-for users wanting to use Heist templates with their application, the other is-designed for users writing Snap Extensions which use their own Heist templates-internally.--The first one of these instances is-@HasHeistState (SnapExtend s) s => MonadHeist (SnapExtend s) (SnapExtend s)@.-This means that any type @s@ which has a 'HeistState', whose-'TemplateState'\'s monad is @SnapExtend s@ forms a 'MonadHeist' whose-'TemplateState'\'s monad is @SnapExtend s@ and whose monad itself is-@SnapExtend s@. The @s@ here is your application's state, and @SnapExtend s@-is your application's monad.--The second one of these instances is-@HasHeistState m s => MonadHeist m (ReaderT s m)@. This means that any type-@s@ which has, for any m, a @HeistState m@, forms a 'MonadHeist', whose-'TemplateState'\'s monad is @m@, when made the environment of-a 'ReaderT' wrapped around @m@. The @s@ here would be the Snap Extension's-internal state, and the @m@ would be 'SnapExtend' wrapped around any @s'@-which was an instance of the Snap Extension's @HasState@ class.--This implementation does not require that your application's monad implement-interfaces from any other Snap Extension.---}--module Snap.Extension.Heist.Impl-  (--    -- * Heist State Definitions-    HeistState-  , HasHeistState(..)-  , heistInitializer--    -- * The MonadHeist Interface-  , MonadHeist(..)--    -- * Convenience Functions-  , registerSplices-  ) where--import           Control.Concurrent.MVar-import           Control.Monad.Reader-import           Data.ByteString (ByteString)-import           Data.Maybe-import           Data.Text (Text)-import           Snap.Extension-import           Snap.Extension.Heist-import           Snap.Types-import           Text.Templating.Heist-import           Text.Templating.Heist.Splices.Static------------------------------------------------------------------------------------ | Your application's state must include a 'HeistState' in order for your--- application to be a 'MonadHeist'.------ Unlike other @-State@ types, this is of kind @(* -> *) -> *@. Unless you're--- developing your own Snap Extension which has its own internal 'HeistState',--- the type argument you want to pass to 'HeistState' is your application's--- monad, which should be 'SnapExtend' wrapped around your application's--- state.-data MonadSnap m => HeistState m = HeistState-    { _path     :: FilePath-    , _origTs   :: TemplateState m-    , _tsMVar   :: MVar (TemplateState m)-    , _sts      :: StaticTagState-    , _modifier :: TemplateState m -> TemplateState m-    }------------------------------------------------------------------------------------ | For your application's monad to be a 'MonadHeist', your application's--- state needs to be an instance of 'HasHeistState'. Minimal complete--- definition: 'getHeistState', 'setHeistState'.------ Unlike other @HasState@ type classes, this is a type class has two--- parameters. Among other things, this means that you will need to enable the--- @FlexibleInstances@ and @MultiParameterTypeClasses@ language extensions to--- be able to create an instance of @HasHeistState@. In most cases, the last--- parameter will as usual be your application's state, and the additional--- first one will be the monad formed by wrapping 'SnapExtend' around your--- application's state.------ However, if you are developing your own Snap Extension which uses its own--- internal 'HeistState', the last parameter will be your Snap Extension's--- internal state, and the additional first parameter will be any monad formed--- by wrapping @SnapExtend@ around a type which has an instance of the--- @HasState@ class for your monad. These two use cases are subtly different,--- which is why 'HasHeistState' needs two type parameters.-class MonadSnap m => HasHeistState m s | s -> m where-    getHeistState :: s -> HeistState m-    setHeistState :: HeistState m -> s -> s--    modifyHeistState :: (HeistState m -> HeistState m) -> s -> s-    modifyHeistState f s = setHeistState (f $ getHeistState s) s------------------------------------------------------------------------------------ | The 'Initializer' for 'HeistState'.-heistInitializer :: MonadSnap m-                 => FilePath-                 -- ^ Path to a template directory containing @.tpl@ files-                 -> (TemplateState m -> TemplateState m)-                 -- ^ Function to modify the initial template state-                 -> Initializer (HeistState m)-heistInitializer p modTS = do-    heistState <- liftIO $ do-        (origTs,sts) <- bindStaticTag $ modTS $ emptyTemplateState p-        loadTemplates p origTs >>= either error (\ts -> do-            tsMVar <- newMVar ts-            return $ HeistState p origTs tsMVar sts id)-    mkInitializer heistState----------------------------------------------------------------------------------instance MonadSnap m => InitializerState (HeistState m) where-    extensionId = const "Heist/Impl"-    mkCleanup   = const $ return ()-    mkReload (HeistState p origTs tsMVar sts _) = do-        clearStaticTagCache $ sts-        either error (modifyMVar_ tsMVar . const . return) =<<-            loadTemplates p origTs----------------------------------------------------------------------------------instance HasHeistState (SnapExtend s) s-      => MonadHeist (SnapExtend s) (SnapExtend s) where-    render t = do-        hs <- asks getHeistState-        renderHelper hs Nothing t--    renderAs c t = do-        hs <- asks getHeistState-        renderHelper hs (Just c) t--    heistLocal f = local $ modifyHeistState $ \s ->-        s { _modifier = f . _modifier s }----------------------------------------------------------------------------------instance HasHeistState m s => MonadHeist m (ReaderT s m) where-    render t = ReaderT $ \s -> renderHelper (getHeistState s) Nothing t--    renderAs c t = ReaderT $ \s -> renderHelper (getHeistState s) (Just c) t--    heistLocal f = local $ modifyHeistState $ \s ->-        s { _modifier = f . _modifier s }----------------------------------------------------------------------------------renderHelper :: (MonadSnap m)-             => HeistState m-             -> Maybe MIMEType-             -> ByteString-             -> m ()-renderHelper hs c t = do-    let (HeistState _ _ tsMVar _ modifier) = hs-    ts <- liftIO $ fmap modifier $ readMVar tsMVar-    renderTemplate ts t >>= maybe pass (\(b,mime) -> do-        modifyResponse $ setContentType $ fromMaybe mime c-        writeBuilder b)------------------------------------------------------------------------------------ | Take your application's state and register these splices in it so--- that you don't have to re-list them in every handler. Should be called from--- inside your application's 'Initializer'.  The splices registered by this--- function do not persist through template reloads.  Those splices must be--- passed as a parameter to heistInitializer.------ (NOTE: In the future we may decide to deprecate registerSplices in favor of--- the heistInitializer argument.)------ Typical use cases are dynamically generated components that are present in--- many of your views.------ Example Usage:------ @--- appInit :: Initializer AppState--- appInit = do---  hs <- heistInitializer \"templates\"---  registerSplices hs $---   [ (\"tabs\", tabsSplice)---   , (\"loginLogout\", loginLogoutSplice) ]--- @-registerSplices-  :: (MonadSnap m, MonadIO n)-  => HeistState m-  -- ^ Heist state that you are going to embed in your application's state.-  -> [(Text, Splice m)]-  -- ^ Your splices.-  -> n ()-registerSplices s sps = liftIO $ do-  let mv = _tsMVar s-  modifyMVar_ mv $ (return . bindSplices sps)
− src/Snap/Extension/Loader/Devel.hs
@@ -1,176 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TemplateHaskell #-}--- | This module includes the machinery necessary to use hint to load--- action code dynamically.  It includes a Template Haskell function--- to gather the necessary compile-time information about code--- location, compiler arguments, etc, and bind that information into--- the calls to the dynamic loader.-module Snap.Extension.Loader.Devel-  ( loadSnapTH-  , loadSnapTH'-  ) where--import           Control.Monad (liftM2)--import           Data.List-import           Data.Maybe (catMaybes)-import           Data.Time.Clock (diffUTCTime, getCurrentTime)--import           Language.Haskell.Interpreter hiding (lift, liftIO)-import           Language.Haskell.Interpreter.Unsafe--import           Language.Haskell.TH--import           System.Environment (getArgs)---------------------------------------------------------------------------------import           Snap.Types-import           Snap.Extension.Loader.Devel.Signal-import           Snap.Extension.Loader.Devel.Evaluator-import           Snap.Extension.Loader.Devel.TreeWatcher----------------------------------------------------------------------------------- | This function derives all the information necessary to use the--- interpreter from the compile-time environment, and compiles it in--- to the generated code.------ This could be considered a TH wrapper around a function------ > loadSnap :: Initializer s -> SnapExtend s () -> [String] -> IO (Snap ())------ with a magical implementation.------ The upshot is that you shouldn't need to recompile your server--- during development unless your .cabal file changes, or the code--- that uses this splice changes.-loadSnapTH :: Name -> Name -> [String] -> Q Exp-loadSnapTH initializer action additionalWatchDirs =-    loadSnapTH' modules imports additionalWatchDirs loadStr-  where-    initMod = nameModule initializer-    initBase = nameBase initializer-    actMod = nameModule action-    actBase = nameBase action--    modules = catMaybes [initMod, actMod]-    imports = ["Snap.Extension"]--    loadStr = intercalate " " [ "runInitializerWithoutReloadAction"-                              , initBase-                              , actBase-                              ]------------------------------------------------------------------------------------ | This is the backing implementation for 'loadSnapTH'.  This--- interface can be used when the types involved don't include a--- SnapExtend and an Initializer.-loadSnapTH' :: [String] -- ^ the list of modules to interpret-            -> [String] -- ^ the list of modules to import in addition-                        -- to those being interpreted-            -> [String] -- ^ additional directories to watch for-                        -- changes to trigger to recompile/reload-            -> String   -- ^ the expression to interpret in the-                        -- context of the loaded modules and imports.-                        -- It should have the type 'HintLoadable'-            -> Q Exp-loadSnapTH' modules imports additionalWatchDirs loadStr = do-    args <- runIO getArgs--    let opts = getHintOpts args-        srcPaths = additionalWatchDirs ++ getSrcPaths args--    [| hintSnap opts modules imports srcPaths loadStr |]------------------------------------------------------------------------------------ | Convert the command-line arguments passed in to options for the--- hint interpreter.  This is somewhat brittle code, based on a few--- experimental datapoints regarding the structure of the command-line--- arguments cabal produces.-getHintOpts :: [String] -> [String]-getHintOpts args = removeBad opts-  where-    bad = ["-threaded", "-O"]-    removeBad = filter (\x -> not $ any (`isPrefixOf` x) bad)--    hideAll = filter (== "-hide-all-packages") args--    srcOpts = filter (\x -> "-i" `isPrefixOf` x-                            && not ("-idist" `isPrefixOf` x)) args--    toCopy = filter (not . isSuffixOf ".hs") $-             dropWhile (not . ("-package" `isPrefixOf`)) args-    copy = map (intercalate " ") . groupBy (\_ s -> not $ "-" `isPrefixOf` s)--    opts = hideAll ++ srcOpts ++ copy toCopy------------------------------------------------------------------------------------ | This function extracts the source paths from the compilation args-getSrcPaths :: [String] -> [String]-getSrcPaths = filter (not . null) . map (drop 2) . filter srcArg-  where-    srcArg x = "-i" `isPrefixOf` x && not ("-idist" `isPrefixOf` x)------------------------------------------------------------------------------------ | This function creates the Snap handler that actually is--- responsible for doing the dynamic loading of actions via hint,--- given all of the configuration information that the interpreter--- needs.  It also ensures safe concurrent access to the interpreter,--- and caches the interpreter results for a short time before allowing--- it to run again.------ Generally, this won't be called manually.  Instead, loadSnapTH will--- generate a call to it at compile-time, calculating all the--- arguments from its environment.-hintSnap :: [String] -- ^ A list of command-line options for the interpreter-         -> [String] -- ^ A list of modules that need to be-                     -- interpreted. This should contain only the-                     -- modules which contain the initialization,-                     -- cleanup, and handler actions.  Everything else-                     -- they require will be loaded transitively.-         -> [String] -- ^ A list of modules that need to be be-                     -- imported, in addition to the ones that need to-                     -- be interpreted.  This only needs to contain-                     -- modules that aren't being interpreted, such as-                     -- those from other libraries, that are used in-                     -- the expression passed in.-         -> [String] -- ^ A list of paths to watch for updates-         -> String   -- ^ The string to execute-         -> IO (Snap (), IO ())-hintSnap opts modules imports srcPaths action =-    protectedHintEvaluator initialize test loader-  where-    interpreter = do-        loadModules . nub $ modules-        setImports . nub $ "Prelude" : "Snap.Types" : imports ++ modules--        interpret action (as :: HintLoadable)--    loadInterpreter = unsafeRunInterpreterWithArgs opts interpreter--    formatOnError (Left err) = error $ format err-    formatOnError (Right a) = a--    loader = formatOnError `fmap` protectHandlers loadInterpreter--    initialize = liftM2 (,) getCurrentTime $ getTreeStatus srcPaths--    test (prevTime, ts) = do-        now <- getCurrentTime-        if diffUTCTime now prevTime < 3-            then return True-            else checkTreeStatus ts------------------------------------------------------------------------------------ | Convert an InterpreterError to a String for presentation-format :: InterpreterError -> String-format (UnknownError e)   = "Unknown interpreter error:\r\n\r\n" ++ e-format (NotAllowed e)     = "Interpreter action not allowed:\r\n\r\n" ++ e-format (GhcException e)   = "GHC error:\r\n\r\n" ++ e-format (WontCompile errs) = "Compile errors:\r\n\r\n" ++-    (intercalate "\r\n" $ nub $ map errMsg errs)-
− src/Snap/Extension/Loader/Devel/Evaluator.hs
@@ -1,145 +0,0 @@-{-# LANGUAGE ScopedTypeVariables #-}--module Snap.Extension.Loader.Devel.Evaluator-  ( HintLoadable-  , protectedHintEvaluator-  ) where---import Control.Exception-import Control.Monad (when)-import Control.Monad.Trans (liftIO)--import Control.Concurrent (ThreadId, forkIO, myThreadId)-import Control.Concurrent.MVar--import Prelude hiding (catch, init, any)--import Snap.Types (Snap)------------------------------------------------------------------------------------ | A type synonym to simply talking about the type loaded by hint.-type HintLoadable = IO (Snap (), IO ())------------------------------------------------------------------------------------ | Convert an action to generate 'HintLoadable's into an action to--- generate Snap actions.  The resulting action will share initialized--- state until the next execution of the input action.  At this time,--- the cleanup action will be executed.------ The first two arguments control when recompiles are done.  The--- first argument is an action that is executed when compilation--- starts.  The second is a function from the result of the first--- action to an action that determines whether the value from the--- previous compilation is still good.  This abstracts out the--- strategy for determining when a cached result is no longer valid.------ If an exception is raised during the processing of the action, it--- will be thrown to all waiting threads, and for all requests made--- before the recompile condition is reached.-protectedHintEvaluator :: forall a.-                          IO a-                       -> (a -> IO Bool)-                       -> IO HintLoadable-                       -> IO (Snap (), IO ())-protectedHintEvaluator start test getInternals = do-    -- The list of requesters waiting for a result.  Contains the-    -- ThreadId in case of exceptions, and an empty MVar awaiting a-    -- successful result.-    readerContainer <- newReaderContainer--    -- Contains the previous result and initialization value, and the-    -- time it was stored, if a previous result has been computed.-    -- The result stored is either the actual result and-    -- initialization result, or the exception thrown by the-    -- calculation.-    resultContainer <- newResultContainer--    -- The model used for the above MVars in the returned action is-    -- "keep them full, unless updating them."  In every case, when-    -- one of those MVars is emptied, the next action is to fill that-    -- same MVar.  This makes deadlocking on MVar wait impossible.-    let snap = do-            let waitForNewResult :: IO (Snap ())-                waitForNewResult = do-                    -- Need to calculate a new result-                    tid <- myThreadId-                    reader <- newEmptyMVar--                    readers <- takeMVar readerContainer--                    -- Some strictness is employed to ensure the MVar-                    -- isn't holding on to a chain of unevaluated thunks.-                    let pair = (tid, reader)-                        newReaders = readers `seq` pair `seq` (pair : readers)-                    putMVar readerContainer $! newReaders--                    -- If this is the first reader to queue, clean up the-                    -- previous state, if there was any, and then begin-                    -- evaluation of the new code and state.-                    when (null readers) $ do-                        let runAndFill = block $ do-                                -- run the cleanup action-                                previous <- readMVar resultContainer-                                unblock $ cleanup previous--                                -- compile the new internals and initialize-                                stateInitializer <- unblock getInternals-                                res <- unblock stateInitializer--                                let a = fst res--                                clearAndNotify (Right res) (flip putMVar a . snd)--                            killWaiting :: SomeException -> IO ()-                            killWaiting e = block $ do-                                clearAndNotify (Left e) (flip throwTo e . fst)-                                throwIO e--                            clearAndNotify r f = do-                                a <- unblock start-                                _ <- swapMVar resultContainer $ Just (r, a)-                                allReaders <- swapMVar readerContainer []-                                mapM_ f allReaders--                        _ <- forkIO $ runAndFill `catch` killWaiting-                        return ()--                    -- Wait for the evaluation of the action to complete,-                    -- and return its result.-                    takeMVar reader--            existingResult <- liftIO $ readMVar resultContainer--            getResult <- liftIO $ case existingResult of-                Just (res, a) -> do-                    -- There's an existing result.  Check for validity-                    valid <- test a-                    case (valid, res) of-                        (True, Right (x, _)) -> return x-                        (True, Left e)       -> throwIO e-                        (False, _)           -> do-                            _ <- swapMVar resultContainer Nothing-                            waitForNewResult-                Nothing -> waitForNewResult-            getResult--        clean = do-             let msg = "invalid dynamic loader state.  " ++-                       "The cleanup action has been executed"-             contents <- swapMVar resultContainer $ error msg-             cleanup contents--    return (snap, clean)-  where-    newReaderContainer :: IO (MVar [(ThreadId, MVar (Snap ()))])-    newReaderContainer = newMVar []--    newResultContainer :: IO (MVar (Maybe (Either SomeException-                                                  (Snap (), IO ()), a)))-    newResultContainer = newMVar Nothing--    cleanup (Just (Right (_, clean), _)) = clean-    cleanup _                            = return ()
− src/Snap/Extension/Loader/Devel/Signal.hs
@@ -1,43 +0,0 @@-{-# LANGUAGE CPP #-}-module Snap.Extension.Loader.Devel.Signal (protectHandlers) where--import Control.Exception (bracket)--#ifdef mingw32_HOST_OS-import GHC.ConsoleHandler as C---saveHandlers :: IO C.Handler-saveHandlers = C.installHandler Ignore---restoreHandlers :: C.Handler -> IO C.Handler-restoreHandlers = C.installHandler---#else-import qualified System.Posix.Signals as S--helper :: S.Handler -> S.Signal -> IO S.Handler-helper handler signal = S.installHandler signal handler Nothing---signals :: [S.Signal]-signals = [ S.sigQUIT-          , S.sigINT-          , S.sigHUP-          , S.sigTERM-          ]---saveHandlers :: IO [S.Handler]-saveHandlers = mapM (helper S.Ignore) signals---restoreHandlers :: [S.Handler] -> IO [S.Handler]-restoreHandlers h = sequence $ zipWith helper h signals---#endif-protectHandlers :: IO a -> IO a-protectHandlers a = bracket saveHandlers restoreHandlers $ const a
− src/Snap/Extension/Loader/Devel/TreeWatcher.hs
@@ -1,41 +0,0 @@-module Snap.Extension.Loader.Devel.TreeWatcher-    ( TreeStatus-    , getTreeStatus-    , checkTreeStatus-    ) where--import Control.Applicative--import System.Directory-import System.Directory.Tree--import System.Time------------------------------------------------------------------------------------ | An opaque representation of the contents and last modification--- times of a forest of directory trees.-data TreeStatus = TS [FilePath] [AnchoredDirTree ClockTime]------------------------------------------------------------------------------------ | Create a 'TreeStatus' for later checking with 'checkTreeStatus'-getTreeStatus :: [FilePath] -> IO TreeStatus-getTreeStatus = liftA2 (<$>) TS readModificationTimes------------------------------------------------------------------------------------ | Checks that all the files present in the initial set of paths are--- the exact set of files currently present, with unchanged modifcations times-checkTreeStatus :: TreeStatus -> IO Bool-checkTreeStatus (TS paths entries) = check <$> readModificationTimes paths-  where-    check = and . zipWith (==) entries------------------------------------------------------------------------------------ | This is the core of the functions in this module.  It converts a--- list of filepaths into a list of 'AnchoredDirTree' annotated with--- the modification times of the files located in those paths.-readModificationTimes :: [FilePath] -> IO [AnchoredDirTree ClockTime]-readModificationTimes = mapM $ readDirectoryWith getModificationTime
− src/Snap/Extension/Server.hs
@@ -1,138 +0,0 @@-{-# LANGUAGE BangPatterns      #-}-{-# LANGUAGE CPP               #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TemplateHaskell   #-}--{-|--This module provides replacements for the 'httpServe' and 'quickHttpServe'-functions exported by 'Snap.Http.Server'. By taking a 'Initializer' as an-argument, these functions simplify the glue code that is needed to use Snap-Extensions.---}--module Snap.Extension.Server-  ( ConfigExtend-  , httpServe-  , quickHttpServe-  , defaultConfig-  , getReloadHandler-  , setReloadHandler-  , module Snap.Http.Server.Config-  ) where--import           Control.Exception (SomeException)-import           Control.Monad-import           Control.Monad.CatchIO-import           Data.ByteString (ByteString)-import           Data.Maybe-import           Data.Monoid-import qualified Data.Text as T-import qualified Data.Text.Encoding as T-import           Prelude hiding (catch)-import           Snap.Extension-import qualified Snap.Http.Server as SS-import qualified Snap.Http.Server.Config as C-import           Snap.Http.Server.Config hiding ( defaultConfig-                                                , completeConfig-                                                , getOther-                                                , setOther-                                                )-import           Snap.Util.GZip-import           Snap.Types-import           System.IO------------------------------------------------------------------------------------ | 'ConfigExtend' is similar to the 'Config' exported by 'Snap.Http.Server',--- but is augmented with a @reloadHandler@ field which can be accessed using--- 'getReloadHandler' and 'setReloadHandler'.-type ConfigExtend s = Config-    (SnapExtend s) (IO [(ByteString, Maybe ByteString)] -> SnapExtend s ())----------------------------------------------------------------------------------getReloadHandler :: ConfigExtend s -> Maybe-                      (IO [(ByteString, Maybe ByteString)] -> SnapExtend s ())-getReloadHandler = C.getOther----------------------------------------------------------------------------------setReloadHandler :: (IO [(ByteString, Maybe ByteString)] -> SnapExtend s ())-                 -> ConfigExtend s-                 -> ConfigExtend s-setReloadHandler = C.setOther-------------------------------------------------------------------------------------- | These are the default values for all the fields in 'ConfigExtend'.------ > hostname      = "localhost"--- > address       = "0.0.0.0"--- > port          = 8000--- > accessLog     = "log/access.log"--- > errorLog      = "log/error.log"--- > locale        = "en_US"--- > compression   = True--- > verbose       = True--- > errorHandler  = prints the error message--- > reloadHandler = prints the result of each reload handler (error/success)----defaultConfig :: ConfigExtend s-defaultConfig = setReloadHandler handler C.defaultConfig-  where-    handler = path "admin/reload" . defaultReloadHandler------------------------------------------------------------------------------------ | Completes a partial 'Config' by filling in the unspecified values with--- the default values from 'defaultConfig'.-completeConfig :: ConfigExtend s -> IO (ConfigExtend s)-completeConfig = C.completeConfig . (mappend defaultConfig)------------------------------------------------------------------------------------ | Starts serving HTTP requests using the given handler, with settings from--- the 'ConfigExtend' passed in. This function never returns; to shut down--- the HTTP server, kill the controlling thread.-httpServe :: ConfigExtend s-          -- ^ Any configuration options which override the defaults-          -> Initializer s-          -- ^ The 'Initializer' function for the application's monad-          -> SnapExtend s ()-          -- ^ The application to be served-          -> IO ()-httpServe config initializer handler = do-    conf <- completeConfig config-    let !verbose  = fromJust $ getVerbose conf-    let !reloader = fromJust $ getReloadHandler conf-    let !compress = if fromJust $ getCompression conf then withCompression else id-    let !catch500 = flip catch $ fromJust $ getErrorHandler conf-    let !serve    = SS.simpleHttpServe config-    (site, cleanup) <- runInitializerWithReloadAction-                         verbose-                         initializer-                         (catch500 $ compress handler)-                         reloader-    _   <- try $ serve $ site :: IO (Either SomeException ())-    putStr "\n"-    cleanup------------------------------------------------------------------------------------ | Starts serving HTTP using the given handler. The configuration is read--- from the options given on the command-line, as returned by--- 'commandLineConfig'.-quickHttpServe :: Initializer s-               -- ^ The 'Initializer' function for the application's monad-               -> SnapExtend s ()-               -- ^ The application to be served-               -> IO ()-quickHttpServe r m = commandLineConfig emptyConfig >>= \c -> httpServe c r m---------------------------------------------------------------------------------fromUTF8 :: ByteString -> String-fromUTF8 = T.unpack . T.decodeUtf8
+ src/Snap/Loader/Devel.hs view
@@ -0,0 +1,180 @@+{-# LANGUAGE TemplateHaskell #-}+-- | This module includes the machinery necessary to use hint to load+-- action code dynamically.  It includes a Template Haskell function+-- to gather the necessary compile-time information about code+-- location, compiler arguments, etc, and bind that information into+-- the calls to the dynamic loader.+module Snap.Loader.Devel+  ( loadSnapTH+  ) where++import           Control.Monad (liftM2)++import           Data.Char (isAlphaNum)+import           Data.List+import           Data.Maybe (maybeToList)+import           Data.Time.Clock (diffUTCTime, getCurrentTime)+import           Data.Typeable++import           Language.Haskell.Interpreter hiding (lift, liftIO, typeOf)+import           Language.Haskell.Interpreter.Unsafe++import           Language.Haskell.TH++import           System.Environment (getArgs)++------------------------------------------------------------------------------+import           Snap.Core+import           Snap.Loader.Devel.Signal+import           Snap.Loader.Devel.Evaluator+import           Snap.Loader.Devel.TreeWatcher++------------------------------------------------------------------------------+-- | This function derives all the information necessary to use the+-- interpreter from the compile-time environment, and compiles it in+-- to the generated code.+--+-- This could be considered a TH wrapper around a function+--+-- > loadSnap :: Typeable a => IO a -> (a -> IO (Snap (), IO ()))+-- >                        -> [String] -> IO (a, Snap (), IO ())+--+-- with a magical implementation.  The [String] argument is a list of+-- directories to watch for updates to trigger a reloading.+-- Directories containing code should be automatically picked up by+-- this splice.+--+-- The generated splice executes the initialiser once, sets up the+-- interpreter for the load function, and returns the initializer's+-- result along with the interpreter's proxy handler and cleanup+-- actions.  The behavior of the proxy actions will change to reflect+-- changes in the watched files, reinterpreting the load function as+-- needed and applying it to the initializer result.+--+-- This will handle reloading the application successfully in most+-- cases.  The cases in which it is certain to fail are those+-- involving changing the types of the initializer or the load+-- function, or changing the compiler options required, such as by+-- changing/adding dependencies in the project's .cabal file.  In+-- those cases, a full recompile will be needed.+loadSnapTH :: Q Exp    -- ^ the initializer expression+           -> Name     -- ^ the name of the load function+           -> [String] -- ^ a list of directories to watch in addition+                       -- to those containing code+           -> Q Exp+loadSnapTH initializer action additionalWatchDirs = do+    args <- runIO getArgs++    let opts = getHintOpts args+        srcPaths = additionalWatchDirs ++ getSrcPaths args++    -- The first line is an extra type check to ensure the provided+    -- names refer to expressions with the correct types+    [| do let _ = $initializer >>= $(varE action)+          v <- $initializer+          (handler, cleanup) <- hintSnap opts actMods srcPaths loadStr v+          return (v, handler, cleanup) |]+  where+    actMods = maybeToList $ nameModule action+    loadStr = nameBase action+++------------------------------------------------------------------------------+-- | Convert the command-line arguments passed in to options for the+-- hint interpreter.  This is somewhat brittle code, based on a few+-- experimental datapoints regarding the structure of the command-line+-- arguments cabal produces.+getHintOpts :: [String] -> [String]+getHintOpts args = removeBad opts+  where+    bad = ["-threaded", "-O"]+    removeBad = filter (\x -> not $ any (`isPrefixOf` x) bad)++    hideAll = filter (== "-hide-all-packages") args++    srcOpts = filter (\x -> "-i" `isPrefixOf` x+                            && not ("-idist" `isPrefixOf` x)) args++    toCopy = filter (not . isSuffixOf ".hs") $+             dropWhile (not . ("-package" `isPrefixOf`)) args+    copy = map (intercalate " ") . groupBy (\_ s -> not $ "-" `isPrefixOf` s)++    opts = hideAll ++ srcOpts ++ copy toCopy+++------------------------------------------------------------------------------+-- | This function extracts the source paths from the compilation args+getSrcPaths :: [String] -> [String]+getSrcPaths = filter (not . null) . map (drop 2) . filter srcArg+  where+    srcArg x = "-i" `isPrefixOf` x && not ("-idist" `isPrefixOf` x)+++------------------------------------------------------------------------------+-- | This function creates the Snap handler that actually is+-- responsible for doing the dynamic loading of actions via hint,+-- given all of the configuration information that the interpreter+-- needs.  It also ensures safe concurrent access to the interpreter,+-- and caches the interpreter results for a short time before allowing+-- it to run again.+--+-- Generally, this won't be called manually.  Instead, loadSnapTH will+-- generate a call to it at compile-time, calculating all the+-- arguments from its environment.+hintSnap :: Typeable a+         => [String] -- ^ A list of command-line options for the interpreter+         -> [String] -- ^ A list of modules that need to be+                     -- interpreted. This should contain only the+                     -- modules which contain the initialization,+                     -- cleanup, and handler actions.  Everything else+                     -- they require will be loaded transitively.+         -> [String] -- ^ A list of paths to watch for updates+         -> String   -- ^ The name of the function to load+         -> a        -- ^ The value to apply the loaded function to+         -> IO (Snap (), IO ())+hintSnap opts modules srcPaths action value =+    protectedHintEvaluator initialize test loader+  where+    witness x = undefined $ x `asTypeOf` value :: HintLoadable++    -- This is somewhat fragile, and probably can be cleaned up with a+    -- future version of Typeable.  For the moment, and+    -- backwards-compatibility, this is the approach being taken.+    witnessModules = map (reverse . drop 1 . dropWhile (/= '.') . reverse) .+                     filter (elem '.') . groupBy typePart . show . typeOf $+                     witness++    typePart x y = (isAlphaNum x && isAlphaNum  y) || x == '.' || y == '.'++    interpreter = do+        loadModules . nub $ modules+        setImports . nub $ "Prelude" : witnessModules ++ modules++        f <- interpret action witness+        return $ f value++    loadInterpreter = unsafeRunInterpreterWithArgs opts interpreter++    formatOnError (Left err) = error $ format err+    formatOnError (Right a) = a++    loader = formatOnError `fmap` protectHandlers loadInterpreter++    initialize = liftM2 (,) getCurrentTime $ getTreeStatus srcPaths++    test (prevTime, ts) = do+        now <- getCurrentTime+        if diffUTCTime now prevTime < 3+            then return True+            else checkTreeStatus ts+++------------------------------------------------------------------------------+-- | Convert an InterpreterError to a String for presentation+format :: InterpreterError -> String+format (UnknownError e)   = "Unknown interpreter error:\r\n\r\n" ++ e+format (NotAllowed e)     = "Interpreter action not allowed:\r\n\r\n" ++ e+format (GhcException e)   = "GHC error:\r\n\r\n" ++ e+format (WontCompile errs) = "Compile errors:\r\n\r\n" +++    (intercalate "\r\n" $ nub $ map errMsg errs)+
+ src/Snap/Loader/Devel/Evaluator.hs view
@@ -0,0 +1,144 @@+{-# LANGUAGE ScopedTypeVariables #-}++module Snap.Loader.Devel.Evaluator+  ( HintLoadable+  , protectedHintEvaluator+  ) where+++import Control.Exception+import Control.Monad (when)+import Control.Monad.Trans (liftIO)++import Control.Concurrent (ThreadId, forkIO, myThreadId)+import Control.Concurrent.MVar++import Prelude hiding (catch, init, any)++import Snap.Core (Snap)+++------------------------------------------------------------------------------+-- | A type synonym to simply talking about the type loaded by hint.+type HintLoadable = IO (Snap (), IO ())+++------------------------------------------------------------------------------+-- | Convert an action to generate 'HintLoadable's into Snap and IO+-- actions that handle periodic reloading.  The resulting action will+-- share initialized state until the next execution of the input+-- action.  At this time, the cleanup action will be executed.+--+-- The first two arguments control when recompiles are done.  The+-- first argument is an action that is executed when compilation+-- starts.  The second is a function from the result of the first+-- action to an action that determines whether the value from the+-- previous compilation is still good.  This abstracts out the+-- strategy for determining when a cached result is no longer valid.+--+-- If an exception is raised during the processing of the action, it+-- will be thrown to all waiting threads, and for all requests made+-- before the recompile condition is reached.+protectedHintEvaluator :: forall a.+                          IO a+                       -> (a -> IO Bool)+                       -> IO HintLoadable+                       -> IO (Snap (), IO ())+protectedHintEvaluator start test getInternals = do+    -- The list of requesters waiting for a result.  Contains the+    -- ThreadId in case of exceptions, and an empty MVar awaiting a+    -- successful result.+    readerContainer <- newReaderContainer++    -- Contains the previous result and initialization value, and the+    -- time it was stored, if a previous result has been computed.+    -- The result stored is either the actual result and+    -- initialization result, or the exception thrown by the+    -- calculation.+    resultContainer <- newResultContainer++    -- The model used for the above MVars in the returned action is+    -- "keep them full, unless updating them."  In every case, when+    -- one of those MVars is emptied, the next action is to fill that+    -- same MVar.  This makes deadlocking on MVar wait impossible.+    let snap = do+            let waitForNewResult :: IO (Snap ())+                waitForNewResult = do+                    -- Need to calculate a new result+                    tid <- myThreadId+                    reader <- newEmptyMVar++                    readers <- takeMVar readerContainer++                    -- Some strictness is employed to ensure the MVar+                    -- isn't holding on to a chain of unevaluated thunks.+                    let pair = (tid, reader)+                        newReaders = readers `seq` pair `seq` (pair : readers)+                    putMVar readerContainer $! newReaders++                    -- If this is the first reader to queue, clean up the+                    -- previous state, if there was any, and then begin+                    -- evaluation of the new code and state.+                    when (null readers) $ do+                        let runAndFill = block $ do+                                -- run the cleanup action+                                previous <- readMVar resultContainer+                                unblock $ cleanup previous++                                -- compile the new internals and initialize+                                stateInitializer <- unblock getInternals+                                res <- unblock stateInitializer++                                let a = fst res++                                clearAndNotify (Right res)+                                               (flip putMVar a . snd)++                            killWaiting :: SomeException -> IO ()+                            killWaiting e = block $ do+                                clearAndNotify (Left e) (flip throwTo e . fst)+                                throwIO e++                            clearAndNotify r f = do+                                a <- unblock start+                                _ <- swapMVar resultContainer $ Just (r, a)+                                allReaders <- swapMVar readerContainer []+                                mapM_ f allReaders++                        _ <- forkIO $ runAndFill `catch` killWaiting+                        return ()++                    -- Wait for the evaluation of the action to complete,+                    -- and return its result.+                    takeMVar reader++            existingResult <- liftIO $ readMVar resultContainer++            getResult <- liftIO $ case existingResult of+                Just (res, a) -> do+                    -- There's an existing result.  Check for validity+                    valid <- test a+                    case (valid, res) of+                        (True, Right (x, _)) -> return x+                        (True, Left e)       -> throwIO e+                        (False, _)           -> waitForNewResult+                Nothing -> waitForNewResult+            getResult++        clean = do+             let msg = "invalid dynamic loader state.  " +++                       "The cleanup action has been executed"+             contents <- swapMVar resultContainer $ error msg+             cleanup contents++    return (snap, clean)+  where+    newReaderContainer :: IO (MVar [(ThreadId, MVar (Snap ()))])+    newReaderContainer = newMVar []++    newResultContainer :: IO (MVar (Maybe (Either SomeException+                                                  (Snap (), IO ()), a)))+    newResultContainer = newMVar Nothing++    cleanup (Just (Right (_, clean), _)) = clean+    cleanup _                            = return ()
+ src/Snap/Loader/Devel/Signal.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE CPP #-}+module Snap.Loader.Devel.Signal (protectHandlers) where++import Control.Exception (bracket)++#ifdef mingw32_HOST_OS+import GHC.ConsoleHandler as C+++saveHandlers :: IO C.Handler+saveHandlers = C.installHandler Ignore+++restoreHandlers :: C.Handler -> IO C.Handler+restoreHandlers = C.installHandler+++#else+import qualified System.Posix.Signals as S++helper :: S.Handler -> S.Signal -> IO S.Handler+helper handler signal = S.installHandler signal handler Nothing+++signals :: [S.Signal]+signals = [ S.sigQUIT+          , S.sigINT+          , S.sigHUP+          , S.sigTERM+          ]+++saveHandlers :: IO [S.Handler]+saveHandlers = mapM (helper S.Ignore) signals+++restoreHandlers :: [S.Handler] -> IO [S.Handler]+restoreHandlers h = sequence $ zipWith helper h signals+++#endif+protectHandlers :: IO a -> IO a+protectHandlers a = bracket saveHandlers restoreHandlers $ const a
+ src/Snap/Loader/Devel/TreeWatcher.hs view
@@ -0,0 +1,41 @@+module Snap.Loader.Devel.TreeWatcher+    ( TreeStatus+    , getTreeStatus+    , checkTreeStatus+    ) where++import Control.Applicative++import System.Directory+import System.Directory.Tree++import System.Time+++------------------------------------------------------------------------------+-- | An opaque representation of the contents and last modification+-- times of a forest of directory trees.+data TreeStatus = TS [FilePath] [AnchoredDirTree ClockTime]+++------------------------------------------------------------------------------+-- | Create a 'TreeStatus' for later checking with 'checkTreeStatus'+getTreeStatus :: [FilePath] -> IO TreeStatus+getTreeStatus = liftA2 (<$>) TS readModificationTimes+++------------------------------------------------------------------------------+-- | Checks that all the files present in the initial set of paths are+-- the exact set of files currently present, with unchanged modifcations times+checkTreeStatus :: TreeStatus -> IO Bool+checkTreeStatus (TS paths entries) = check <$> readModificationTimes paths+  where+    check = and . zipWith (==) entries+++------------------------------------------------------------------------------+-- | This is the core of the functions in this module.  It converts a+-- list of filepaths into a list of 'AnchoredDirTree' annotated with+-- the modification times of the files located in those paths.+readModificationTimes :: [FilePath] -> IO [AnchoredDirTree ClockTime]+readModificationTimes = mapM $ readDirectoryWith getModificationTime
+ src/Snap/Loader/Prod.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE TemplateHaskell #-}+module Snap.Loader.Prod+  ( loadSnapTH+  ) where++import           Language.Haskell.TH+++------------------------------------------------------------------------------+-- | This function provides a non-magical type-compatible loader for+-- the one in Snap.Loader.Devel, allowing switching one import to+-- provide production-mode compilation.+--+-- This could be considered a TH wrapper around a function+--+-- > loadSnap :: Typeable a => IO a -> (a -> IO (Snap (), IO ()))+-- >                        -> [String] -> IO (a, Snap (), IO ())+--+-- The third argument is unused, and only present for+-- type-compatibility with Snap.Loader.Devel+loadSnapTH :: Q Exp -> Name -> [String] -> Q Exp+loadSnapTH initializer action _additionalWatchDirs =+    [| do value <- $initializer+          (site, conf) <- $(varE action) value+          return (value, site, conf) |]
+ src/Snap/Snaplet.hs view
@@ -0,0 +1,330 @@+{-# LANGUAGE BangPatterns               #-}+{-# LANGUAGE ExistentialQuantification  #-}+{-# LANGUAGE FlexibleInstances          #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses      #-}+{-# LANGUAGE RankNTypes                 #-}+{-# LANGUAGE TypeOperators              #-}++{-|++Snaplets allow you to build web applications out of composable parts. This+allows you to build self-contained units and glue them together to make your+overall application.++A snaplet has a few moving parts, some user-defined and some provided by the+snaplet API:++  * each snaplet has its own configuration given to it at startup.++  * each snaplet is given its own directory on the filesystem, from which it+    reads its configuration and in which it can store files.++  * each snaplet comes with an 'Initializer' which defines how to create an+    instance of the Snaplet at startup. The initializer decides how to+    interpret the snaplet configuration, which URLs to handle (and how), sets+    up the initial snaplet state, tells the snaplet runtime system how to+    clean the snaplet up, etc.++  * each snaplet contains some user-defined in-memory state; for instance, a+    snaplet that talks to a database might contain a reference to a connection+    pool. The snaplet state is an ordinary Haskell record, with a datatype+    defined by the snaplet author. The initial state record is created during+    initialization and is available to snaplet 'Handler's when serving HTTP+    requests.++NOTE: This documentation is written as a prose tutorial of the snaplets+API.  Don't be scared by the fact that it's auto-generated and is filled with+type signatures.  Just keep reading.++-}++module Snap.Snaplet+  (+  -- * Snaplet+  -- $snapletDoc+    Snaplet+  , SnapletConfig++  -- * Snaplet Helper Functions+  -- $snapletHelpers+  , snapletConfig+  , snapletValue+  , subSnaplet++  -- * Lenses+  -- $lenses++  -- * MonadSnaplet+  -- $monadSnaplet+  , MonadSnaplet(..)+  , getSnapletAncestry+  , getSnapletFilePath+  , getSnapletName+  , getSnapletDescription+  , getSnapletUserConfig+  , getSnapletRootURL++  -- * Snaplet state manipulation+  -- $snapletState+  , getSnapletState+  , putSnapletState+  , modifySnapletState+  , getsSnapletState++--  , wrap+--  , wrapTop++  -- * Initializer+  -- $initializer+  , Initializer+  , SnapletInit+  , makeSnaplet++  , nestSnaplet+  , embedSnaplet+  , nameSnaplet++  , onUnload+  , addPostInitHook+  , addPostInitHookBase+  , printInfo++  -- * Routes+  -- $routes+  , addRoutes+  , wrapHandlers++  -- * Handlers+  , Handler+  , reloadSite++  -- * Serving Applications+  , runSnaplet+  , combineConfig+  , serveSnaplet+  ) where+++import           Snap.Snaplet.Internal.Initializer+import           Snap.Snaplet.Internal.Types++-- $snapletDoc+-- The heart of the snaplets infrastructure is state management. (Note: when+-- we say \"state\" here, we mean in-memory Haskell objects, not external data+-- storage or databases; how you deal with persisted data is up to you.) Most+-- nontrivial pieces of a web application need some kind of runtime state or+-- environment data. The datatype we use to handle this is called 'Snaplet':++-- $snapletHelpers+--+-- Your web application will itself get wrapped in a 'Snaplet', and the+-- top-level user state of your application (which will likely contain other+-- snaplets nested inside it) will look something like this:+--+-- > data App = App+-- >     { _foo                :: Snaplet Foo+-- >     , _bar                :: Snaplet Bar+-- >     , _someNonSnapletData :: String+-- >     }+--+-- Every web application using snaplets has a top-most user state which+-- contains all of the application state; we call this state the \"base\"+-- state.+--+-- We provide a couple of helper functions for working with Snaplet types.++-- $lenses+-- In the example above, the @Foo@ snaplet has to be written to work with any+-- base state (otherwise it wouldn't be reusable!), but functions written to+-- work with the @Foo@ snaplet want to be able to modify the @Foo@ record+-- /within the context/ of the base state. Given that Haskell datatypes are+-- pure, how do you allow for this?+--+-- Our solution is to use /lenses/, as defined in the @data-lens@ library+-- (<http://hackage.haskell.org/package/data-lens>). A lens, notated as+-- follows:+--+-- > Lens a b+--+-- is a \"getter\" and a \"setter\" rolled up into one. The @data-lens@+-- library provides the following functions:+--+-- > getL :: (Lens a b) -> a -> b+-- > setL :: (Lens a b) -> b -> a -> a+-- > modL :: (Lens a b) -> (b -> b) -> a -> a+--+-- which allow you to get, set, and modify a value of type @b@ within the+-- context of type of type @a@. The @data-lens@ package comes with a Template+-- Haskell function called 'makeLenses', which auto-magically defines a lens+-- for every record field having a name beginning with an underscore. In the+-- @App@ example above, adding the declaration:+--+-- > makeLenses [''App]+--+-- would define lenses:+--+-- > foo                :: Lens App (Snaplet Foo)+-- > bar                :: Lens App (Snaplet Bar)+-- > someNonSnapletData :: Lens App String+--+-- The coolest thing about @data-lens@ lenses is that they /compose/, using+-- the "Control.Category"'s generalization of the @(.)@ operator. If the @Foo@+-- type had a field of type @Quux@ within it with a lens @quux :: Lens Foo+-- Quux@, then you could create a lens of type @Lens App Quux@ by composition:+--+-- > import Control.Category+-- > import Prelude hiding ((.))    -- you have to hide (.) from the Prelude+-- >                                -- to use Control.Category.(.)+-- >+-- > data Foo = Foo { _quux :: Quux }+-- > makeLenses [''Foo]+-- >+-- > -- snapletValue is defined in the framework:+-- > snapletValue :: Lens (Snaplet a) a+-- >+-- > appQuuxLens :: Lens App (Snaplet Quux)+-- > appQuuxLens = quux . snapletValue . foo+--+-- Lens composition is very similar to function composition, but it gives you+-- a composed getter and setter at the same time.++-- $monadSnaplet+-- The primary abstraction in the snaplet infrastructure is a combination of+-- the reader and state monads.  The state monad holds the top level+-- application data type (from now on referred to as the base state).  The+-- reader monad holds a lens from the base state to the current snaplet's+-- state.  This allows quux snaplet functions to access and modify the Quux+-- data structure without knowing anything about the App or Foo data+-- structures. It also lets other snaplets call functions from the quux+-- snaplet if they have the quux snaplet's lens @Lens App (Snaplet Quux)@.+-- We can view our application as a tree of snaplets and other pieces of data.+-- The lenses are like pointers to nodes of the tree. If you have a pointer to+-- a node, you can access the node and all of its children without knowing+-- anything about the rest of the tree.+--+-- Several monads use this infrastructure. These monads need at least three+-- type parameters. Two for the lens type, and the standard \'a\' denoting the+-- monad return value. You will usually see this written in type signatures as+-- \"m b v a\" or some variation. The \'m\' is the type variable of the+-- MonadSnaplet type class. \'b\' is the base state, and \'v\' is the state of+-- the current \"view\" snaplet (or simply, current state).+--+-- The MonadSnaplet type class distills the essence of the operations used+-- with this pattern.  Its functions define fundamental methods for navigating+-- snaplet trees.++-- $snapletState+-- MonadSnaplet instances will typically have @MonadState v@ instances.  We+-- provide the following convenience functions which give the equivalent to+-- @MonadState (Snaplet v)@ for the less common cases where you need to work+-- with the Snaplet wrapper.++-- $initializer+-- The Initializer monad is where your application's initialization happens.+-- Initializers are run at startup and any time a site reload is triggered.+-- The Initializer's job is to construct a snaplet's routes and initial state,+-- set up filesystem data, read config files, etc.+--+-- In order to initialize its state, a snaplet needs to initialize all the+-- @Snaplet a@ state for each of its subsnaplets.  The only way to construct+-- a @Snaplet a@ type is by calling 'nestSnaplet' or 'embedSnaplet' from+-- within an initializer.+++-- $writingSnaplets+-- When writing a snaplet, you must define an initializer function.  The+-- initializer function for the Foo snaplet (where Foo is the snaplet's+-- state type) must have a return type of @Initializer b Foo Foo@.+-- To create an initializer like this, you have to use the 'makeSnaplet'+-- function.  It takes care of the necessary internal bookkeeping needed when+-- initializing a new snaplet.  Haskell's strong type system allows us to+-- ensure that calling 'makeSnaplet' is the only way you can construct a+-- Snaplet type.+++-- $routes+-- Snaplet initializers are also responsible for setting up any routes defined+-- by the snaplet.  To do that you'll usually use either 'addRoutes' or+-- 'wrapHandlers'.+++{-+++/FIXME/: finish this section+++Discuss:++  * lenses and how snaplet apps are built out of parts.++  * the initializer type, and what you can do with it++  * layout of snaplets on disk, and how on-disk stuff can be auto-populated+    from the cabal data directory++  * the handler type, and what you can do with it+++++{FIXME: strike/rewrite these sentences. Components that do not need any kind+of state or environment are probably more appropriate as a standalone+library than as a snaplet.++We start our application by defining a data structure to hold the state.+This data structure includes the state of any snaplets (wrapped in a+Snaplet) we want to use as well as any other state we might want.}++> module MyApp where+> import Snap.Snaplet+> import Snap.Snaplet.Heist+>+> data App = App+>     { _heist       :: Snaplet (Heist App)+>     , _foo         :: Snaplet Foo+>     , _bar         :: Snaplet Bar+>     , _companyName :: String+>     }+>+> makeLenses [''App]++The next thing we need to do is define an initializer.++> app :: Initializer App App App+> app = do+>     hs <- nestSnaplet "heist" $ heistInit "templates"+>     fs <- nestSnaplet "foo" $ fooInit heist+>     bs <- nestSnaplet "" $ nameSnaplet "baz" $ barInit heist+>     addRoutes [ ("/hello", writeText "hello world")+>               ]+>     wrapHandlers (<|> with heist heistServe)+>     return $ App hs fs bs "fooCorp"++Then we define a simple main to run the application.++> main = serveSnaplet defaultConfig app++++Snaplet filesystem directory structure:++> snaplet+>   |-- snaplet.cabal+>   |-- log/+>   |-- src/+> ------------------------+>   |-- db.cfg+>   |-- snaplet.cfg+>   |-- public/+>       |-- stylesheets/+>       |-- images/+>       |-- js/+>   |-- snaplets+>       |-- subsnaplet1/+>       |-- subsnaplet2/+>   |-- templates/++-}+
+ src/Snap/Snaplet/Auth.hs view
@@ -0,0 +1,74 @@+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE OverloadedStrings #-}++{-|++  This module contains all the central authentication functionality.++  It exports a number of high-level functions to be used directly in your+  application handlers.++  We also export a number of mid-level functions that+  should be helpful when you are integrating with another way of confirming+  the authentication of login requests.++-}++module Snap.Snaplet.Auth+  (++  -- * Higher Level Handler Functions+    createUser+  , usernameExists+  , saveUser+  , destroyUser+  , loginByUsername+  , loginByRememberToken+  , forceLogin+  , logout+  , currentUser+  , isLoggedIn++  -- * Lower Level Functions+  , markAuthSuccess+  , markAuthFail+  , checkPasswordAndLogin++  -- * Types+  , AuthManager(..)+  , IAuthBackend(..)+  , AuthSettings(..)+  , defAuthSettings+  , AuthUser(..)+  , defAuthUser+  , UserId(..)+  , Password(..)+  , AuthFailure(..)+  , BackendError(..)+  , Role(..)++  -- * Other Utilities+  , withBackend+  , encryptPassword+  , checkPassword+  , authenticatePassword+  , setPassword++  -- * Handlers+  , registerUser+  , loginUser+  , logoutUser+  , requireUser++  -- * Splice helpers+  , addAuthSplices+  , ifLoggedIn+  , ifLoggedOut+  )+  where++import Snap.Snaplet.Auth.AuthManager+import Snap.Snaplet.Auth.Handlers+import Snap.Snaplet.Auth.SpliceHelpers+import Snap.Snaplet.Auth.Types+
+ src/Snap/Snaplet/Auth/AuthManager.hs view
@@ -0,0 +1,102 @@++{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}++module Snap.Snaplet.Auth.AuthManager++(+  -- * AuthManager Datatype+    AuthManager(..)++  -- * Backend Typeclass+  , IAuthBackend(..)++  -- * Context-free Operations+  , buildAuthUser++) where+++import           Data.ByteString (ByteString)+import           Data.Lens.Lazy+import           Data.Time+import           Data.Text (Text)+import           Web.ClientSession++import           Snap.Snaplet+import           Snap.Snaplet.Session+import           Snap.Snaplet.Auth.Types++------------------------------------------------------------------------------+-- | Create a new user from just a username and password+--+-- May throw a "DuplicateLogin" if given username is not unique+buildAuthUser+  :: (IAuthBackend r)+  => r+  -- ^ An auth backend+  -> Text+  -- ^ Username+  -> ByteString+  -- ^ Password+  -> IO AuthUser+buildAuthUser r unm pass = do+  now <- getCurrentTime+  let au = defAuthUser {+              userLogin = unm+            , userPassword = Nothing+            , userCreatedAt = Just now+            , userUpdatedAt = Just now+            }+  au' <- setPassword au pass+  save r au'+++------------------------------------------------------------------------------+-- | All storage backends need to implement this typeclass+--+-- Backend operations may throw 'BackendError's+class IAuthBackend r where++  -- | Needs to create or update the given 'AuthUser' record+  save :: r -> AuthUser -> IO AuthUser++  lookupByUserId :: r -> UserId -> IO (Maybe AuthUser)++  lookupByLogin :: r -> Text -> IO (Maybe AuthUser)++  lookupByRememberToken :: r -> Text -> IO (Maybe AuthUser)++  destroy :: r -> AuthUser -> IO ()+++------------------------------------------------------------------------------+-- | Abstract data type holding all necessary information for auth operation+data AuthManager b = forall r. IAuthBackend r => AuthManager {+    backend :: r+  -- ^ Storage back-end++  , session :: Lens b (Snaplet SessionManager)+  -- ^ A lens pointer to a SessionManager++  , activeUser :: Maybe AuthUser+  -- ^ A per-request logged-in user cache++  , minPasswdLen :: Int+  -- ^ Password length range++  , rememberCookieName :: ByteString+  -- ^ Cookie name for the remember token++  , rememberPeriod :: Maybe Int+  -- ^ Remember period in seconds. Defaults to 2 weeks.++  , siteKey :: Key+  -- ^ A unique encryption key used to encrypt remember cookie++  , lockout :: Maybe (Int, NominalDiffTime)+  -- ^ Lockout after x tries, re-allow entry after y seconds+  }+
+ src/Snap/Snaplet/Auth/Backends/JsonFile.hs view
@@ -0,0 +1,335 @@+{-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE FlexibleInstances #-}+++module Snap.Snaplet.Auth.Backends.JsonFile+  ( initJsonFileAuthManager+  , mkJsonAuthMgr+  ) where+++import           Control.Applicative+import           Control.Monad.CatchIO (throw)+import           Control.Monad.State+import           Control.Concurrent.STM+import           Data.Aeson+import qualified Data.Attoparsec as Atto+import qualified Data.ByteString.Lazy as LB+import qualified Data.ByteString as B+import qualified Data.Map as HM+import           Data.Map (Map)+import           Data.Maybe (fromJust, isJust)+import           Data.Text (Text)+import qualified Data.Text as T+import           Data.Lens.Lazy+import           Data.Time+import           Web.ClientSession+import           System.Directory++import           Snap.Snaplet+import           Snap.Snaplet.Auth.Types+import           Snap.Snaplet.Auth.AuthManager+import           Snap.Snaplet.Session++++------------------------------------------------------------------------------+-- | Initialize a JSON file backed 'AuthManager'+initJsonFileAuthManager+  :: AuthSettings+  -- ^ Authentication settings for your app+  -> Lens b (Snaplet SessionManager)+  -- ^ Lens into a 'SessionManager' auth snaplet will use+  -> FilePath+  -- ^ Where to store user data as JSON+  -> SnapletInit b (AuthManager b)+initJsonFileAuthManager s l db =+  makeSnaplet "JsonFileAuthManager"+      "A snaplet providing user authentication using a JSON-file backend"+      Nothing $ liftIO $ do+    key <- getKey (asSiteKey s)+    jsonMgr <- mkJsonAuthMgr db+    return $ AuthManager {+        backend = jsonMgr+      , session = l+      , activeUser = Nothing+      , minPasswdLen = asMinPasswdLen s+      , rememberCookieName = asRememberCookieName s+      , rememberPeriod = asRememberPeriod s+      , siteKey = key+      , lockout = asLockout s+    }+++------------------------------------------------------------------------------+-- | Load/create a datafile into memory cache and return the manager.+--+-- This data type can be used by itself for batch/non-handler processing.+mkJsonAuthMgr :: FilePath -> IO JsonFileAuthManager+mkJsonAuthMgr fp = do+  db <- loadUserCache fp+  let db' = case db of+              Left e -> error e+              Right x -> x+  cache <- newTVarIO db'+  return $ JsonFileAuthManager {+      memcache = cache+    , dbfile = fp+  }+++type UserIdCache = Map UserId AuthUser+++instance ToJSON UserIdCache where+  toJSON m = toJSON $ HM.toList m+++instance FromJSON UserIdCache where+  parseJSON = fmap HM.fromList . parseJSON+++type LoginUserCache = Map Text UserId+++type RemTokenUserCache = Map Text UserId+++-- JSON user back-end stores the user data and indexes for login and token+-- based logins.+data UserCache = UserCache {+    uidCache    :: UserIdCache          -- the actual datastore+  , loginCache  :: LoginUserCache       -- fast lookup for login field+  , tokenCache  :: RemTokenUserCache    -- fast lookup for remember tokens+  , uidCounter  :: Int                  -- user id counter+}+++defUserCache :: UserCache+defUserCache = UserCache {+    uidCache = HM.empty+  , loginCache = HM.empty+  , tokenCache = HM.empty+  , uidCounter = 0+}+++loadUserCache :: FilePath -> IO (Either String UserCache)+loadUserCache fp = do+  chk <- doesFileExist fp+  case chk of+    True -> do+      d <- B.readFile fp+      case Atto.parseOnly json d of+        Left e -> return . Left $ "Can't open JSON auth backend. Error: " ++ e+        Right v -> case fromJSON v of+          Error e -> return . Left $+              "Malformed JSON auth data store. Error: " ++ e+          Success db -> return $ Right db+    False -> do+      putStrLn "User JSON datafile not found. Creating a new one."+      return $ Right defUserCache+++data JsonFileAuthManager = JsonFileAuthManager {+    memcache :: TVar UserCache+  , dbfile :: FilePath+}+++instance IAuthBackend JsonFileAuthManager where++  save mgr u = do+    now <- getCurrentTime+    oldByLogin <- lookupByLogin mgr (userLogin u)+    oldById <- case userId u of+      Nothing -> return Nothing+      Just x -> lookupByUserId mgr x+    res <- atomically $ do+      cache <- readTVar (memcache mgr)+      res <- case userId u of+        Nothing -> create cache now oldByLogin+        Just _ -> update cache now oldById+      case res of+        Left e -> return $ Left e+        Right (cache', u') -> do+          writeTVar (memcache mgr) cache'+          return $ Right (cache', u')+    case res of+      Left e -> throw e+      Right (cache', u') -> do+        dumpToDisk cache'+        return u'+    where+      create+        :: UserCache+        -> UTCTime+        -> (Maybe AuthUser)+        -> STM (Either BackendError (UserCache, AuthUser))+      create cache now old = do+        case old of+          Just _ -> return $ Left DuplicateLogin+          Nothing -> do+            new <- do+              let uid' = UserId . showT $ uidCounter cache + 1+              let u' = u { userUpdatedAt = Just now, userId = Just uid' }+              return $ cache {+                uidCache = HM.insert uid' u' $ uidCache cache+              , loginCache = HM.insert (userLogin u') uid' $ loginCache cache+              , tokenCache = case userRememberToken u' of+                                Nothing -> tokenCache cache+                                Just x -> HM.insert x uid' $ tokenCache cache+              , uidCounter = uidCounter cache + 1+              }+            return $ Right (new, getLastUser new)+++      -- lookup old record, see what's changed and update indexes accordingly+      update+        :: UserCache+        -> UTCTime+        -> (Maybe AuthUser)+        -> STM (Either BackendError (UserCache, AuthUser))+      update cache now old =+        case old of+          Nothing -> return $ Left $+                       BackendError "User not found; should never happen"+          Just x -> do+            let oldLogin = userLogin x+            let oldToken = userRememberToken x+            let uid = fromJust $ userId u+            let newLogin = userLogin u+            let newToken = userRememberToken u+            let lc = if oldLogin /= userLogin u+                      then HM.insert newLogin uid . HM.delete oldLogin $+                               loginCache cache+                      else loginCache cache+            let tc = if oldToken /= newToken && isJust oldToken+                      then HM.delete (fromJust oldToken) $ loginCache cache+                      else tokenCache cache+            let tc' = case newToken of+                        Just t -> HM.insert t uid tc+                        Nothing -> tc+            let u' = u { userUpdatedAt = Just now }+            let new = cache {+                          uidCache = HM.insert uid u' $ uidCache cache+                        , loginCache = lc+                        , tokenCache = tc'+                      }+            return $ Right (new, u')++      -- Sync user database to disk+      -- Need to implement a mutex here; simult syncs could screw things up+      dumpToDisk c = LB.writeFile (dbfile mgr) (encode c)++      -- Get's the last added user+      getLastUser cache = maybe e id $ getUser cache uid+        where uid = UserId . showT $ uidCounter cache+              e = error "getLastUser failed. This should not happen."+++  destroy = error "JsonFile: destroy is not yet implemented"++  lookupByUserId mgr uid = withCache mgr f+    where f cache = getUser cache uid++  lookupByLogin mgr login = withCache mgr f+    where+      f cache = getUid >>= getUser cache+        where getUid = HM.lookup login (loginCache cache)++  lookupByRememberToken mgr token = withCache mgr f+    where+      f cache = getUid >>= getUser cache+        where getUid = HM.lookup token (tokenCache cache)+++withCache :: JsonFileAuthManager -> (UserCache -> a) -> IO a+withCache mgr f = atomically $ do+  cache <- readTVar $ memcache mgr+  return $ f cache+++getUser :: UserCache -> UserId -> Maybe AuthUser+getUser cache uid = HM.lookup uid (uidCache cache)+++------------------------------------------------------------------------------+-- JSON Instances+--+------------------------------------------------------------------------------+++instance ToJSON UserCache where+  toJSON uc = object+    [ "uidCache"   .= uidCache uc+    , "loginCache" .= loginCache uc+    , "tokenCache" .= tokenCache uc+    , "uidCounter" .= uidCounter uc]+++instance FromJSON UserCache where+  parseJSON (Object v) =+    UserCache+      <$> v .: "uidCache"+      <*> v .: "loginCache"+      <*> v .: "tokenCache"+      <*> v .: "uidCounter"+  parseJSON _ = error "Unexpected JSON input"++instance ToJSON AuthUser where+  toJSON u = object+    [ "uid" .= userId u+    , "login" .= userLogin u+    , "pw" .= userPassword u+    , "activated_at" .= userActivatedAt u+    , "suspended_at" .= userSuspendedAt u+    , "remember_token" .= userRememberToken u+    , "login_count" .= userLoginCount u+    , "failed_login_count" .= userFailedLoginCount u+    , "locked_until" .= userLockedOutUntil u+    , "current_login_at" .= userCurrentLoginAt u+    , "last_login_at" .= userLastLoginAt u+    , "current_ip" .= userCurrentLoginIp u+    , "last_ip" .= userLastLoginIp u+    , "created_at" .= userCreatedAt u+    , "updated_at" .= userUpdatedAt u+    , "meta" .= userMeta u ]+++instance FromJSON AuthUser where+  parseJSON (Object v) = AuthUser+    <$> v .: "uid"+    <*> v .: "login"+    <*> v .: "pw"+    <*> v .: "activated_at"+    <*> v .: "suspended_at"+    <*> v .: "remember_token"+    <*> v .: "login_count"+    <*> v .: "failed_login_count"+    <*> v .: "locked_until"+    <*> v .: "current_login_at"+    <*> v .: "last_login_at"+    <*> v .: "current_ip"+    <*> v .: "last_ip"+    <*> v .: "created_at"+    <*> v .: "updated_at"+    <*> return []+    <*> v .: "meta"+  parseJSON _ = error "Unexpected JSON input"+++instance ToJSON Password where+  toJSON (Encrypted x) = toJSON x+  toJSON (ClearText _) =+      error "ClearText passwords can't be serialized into JSON"+++instance FromJSON Password where+  parseJSON = fmap Encrypted . parseJSON+++showT :: Int -> Text+showT = T.pack . show
+ src/Snap/Snaplet/Auth/Handlers.hs view
@@ -0,0 +1,437 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE RankNTypes #-}++{-|++  Pre-packaged Handlers that deal with form submissions and standard use-cases+  involving authentication.++-}++module Snap.Snaplet.Auth.Handlers where++import           Control.Applicative+import           Control.Monad.CatchIO (throw)+import           Control.Monad.State+import           Data.ByteString (ByteString)+import           Data.Lens.Lazy+import           Data.Maybe (isJust)+import           Data.Serialize hiding (get)+import           Data.Time+import           Data.Text.Encoding (decodeUtf8)+import           Data.Text (Text)+import           Web.ClientSession++import           Snap.Core+import           Snap.Snaplet+import           Snap.Snaplet.Auth.AuthManager+import           Snap.Snaplet.Auth.Types+import           Snap.Snaplet.Session+import           Snap.Snaplet.Session.Common+import           Snap.Snaplet.Session.SecureCookie++++------------------------------------------------------------------------------+-- Higher level functions+------------------------------------------------------------------------------+++------------------------------------------------------------------------------+-- | Create a new user from just a username and password+--+-- May throw a "DuplicateLogin" if given username is not unique+createUser+  :: Text -- Username+  -> ByteString -- Password+  -> Handler b (AuthManager b) AuthUser+createUser unm pwd = withBackend (\r -> liftIO $ buildAuthUser r unm pwd)++------------------------------------------------------------------------------+-- | Check whether a user with the given username exists.+usernameExists+  :: Text+  -- ^ The username to be checked+  -> Handler b (AuthManager b) Bool+usernameExists username = withBackend $+    \r -> liftIO $ isJust <$> lookupByLogin r username++------------------------------------------------------------------------------+-- | Lookup a user by her username, check given password and perform login+loginByUsername+  :: ByteString       -- ^ Username/login for user+  -> Password         -- ^ Should be ClearText+  -> Bool             -- ^ Set remember token?+  -> Handler b (AuthManager b) (Either AuthFailure AuthUser)+loginByUsername _ (Encrypted _) _ =+  error "Cannot login with encrypted password"+loginByUsername unm pwd rm = do+  sk <- gets siteKey+  cn <- gets rememberCookieName+  rp <- gets rememberPeriod+  withBackend $ loginByUsername' sk cn rp+  where+    loginByUsername' :: (IAuthBackend t)+                     => Key -> ByteString -> Maybe Int -> t+                     -> Handler b (AuthManager b)+                                (Either AuthFailure AuthUser)+    loginByUsername' sk cn rp r = do+      au <- liftIO $ lookupByLogin r (decodeUtf8 unm)+      case au of+        Nothing  -> return $ Left UserNotFound+        Just au' -> do+          res <- checkPasswordAndLogin au' pwd+          case res of+            Left e -> return $ Left e+            Right au'' -> do+              case rm of+                True -> do+                  token <- liftIO $ randomToken 64+                  setRememberToken sk cn rp token+                  let au''' = au''+                          { userRememberToken = Just (decodeUtf8 token) }+                  saveUser au'''+                  return $ Right au'''+                False -> return $ Right au''+++------------------------------------------------------------------------------+-- | Remember user from the remember token if possible and perform login+loginByRememberToken :: Handler b (AuthManager b) (Maybe AuthUser)+loginByRememberToken = withBackend $ \r -> do+  sk <- gets siteKey+  rc <- gets rememberCookieName+  rp <- gets rememberPeriod+  token <- getRememberToken sk rc rp+  au <- maybe (return Nothing)+              (liftIO . lookupByRememberToken r . decodeUtf8) token+  case au of+    Just au' -> forceLogin au' >> return au+    Nothing -> return Nothing+++------------------------------------------------------------------------------+-- | Logout the active user+logout :: Handler b (AuthManager b) ()+logout = do+  s <- gets session+  withTop s $ withSession s removeSessionUserId+  rc <- gets rememberCookieName+  forgetRememberToken rc+  modify (\mgr -> mgr { activeUser = Nothing } )+++------------------------------------------------------------------------------+-- | Return the current user; trying to remember from cookie if possible.+currentUser :: Handler b (AuthManager b) (Maybe AuthUser)+currentUser = cacheOrLookup $ withBackend $ \r -> do+  s <- gets session+  uid <- withTop s getSessionUserId+  case uid of+    Nothing -> loginByRememberToken+    Just uid' -> liftIO $ lookupByUserId r uid'+++------------------------------------------------------------------------------+-- | Convenience wrapper around 'rememberUser' that returns a bool result+isLoggedIn :: Handler b (AuthManager b) Bool+isLoggedIn = isJust `fmap` currentUser+++------------------------------------------------------------------------------+-- | Create or update a given user+--+-- May throw a 'BackendError' if something goes wrong.+saveUser :: AuthUser -> Handler b (AuthManager b) AuthUser+saveUser u = withBackend $ liftIO . flip save u+++------------------------------------------------------------------------------+-- | Destroy the given user+--+-- May throw a 'BackendError' if something goes wrong.+destroyUser :: AuthUser -> Handler b (AuthManager b) ()+destroyUser u = withBackend $ liftIO . flip destroy u+++------------------------------------------------------------------------------+--  Lower level helper functions+--+------------------------------------------------------------------------------+++------------------------------------------------------------------------------+-- | Mutate an 'AuthUser', marking failed authentication+--+-- This will save the user to the backend.+markAuthFail :: AuthUser -> Handler b (AuthManager b) AuthUser+markAuthFail u = withBackend $ \r -> do+  lo <- gets lockout+  incFailCtr u >>= checkLockout lo >>= liftIO . save r+  where+    incFailCtr u' = return $ u'+                      { userFailedLoginCount = userFailedLoginCount u' + 1}+    checkLockout lo u' = case lo of+      Nothing          -> return u'+      Just (mx, wait)  ->+        if userFailedLoginCount u' >= mx+          then do+            now <- liftIO getCurrentTime+            let reopen = addUTCTime wait now+            return $ u' { userLockedOutUntil = Just reopen }+          else return u'+++------------------------------------------------------------------------------+-- | Mutate an 'AuthUser', marking successful authentication+--+-- This will save the user to the backend.+markAuthSuccess :: AuthUser -> Handler b (AuthManager b) AuthUser+markAuthSuccess u = withBackend $ \r -> do+  incLoginCtr u >>= updateIp >>= updateLoginTS+    >>= resetFailCtr >>= liftIO . save r+  where+    incLoginCtr u' = return $ u' { userLoginCount = userLoginCount u' + 1 }+    updateIp u' = do+      ip <- rqRemoteAddr `fmap` getRequest+      return $ u' { userLastLoginIp = userCurrentLoginIp u'+                  , userCurrentLoginIp = Just ip }+    updateLoginTS u' = do+      now <- liftIO getCurrentTime+      return $+        u' { userCurrentLoginAt = Just now+           , userLastLoginAt = userCurrentLoginAt u' }+    resetFailCtr u' = return $+      u' { userFailedLoginCount = 0+         , userLockedOutUntil = Nothing }+++------------------------------------------------------------------------------+-- | Authenticate and log the user into the current session if successful.+--+-- This is a mid-level function exposed to allow roll-your-own ways of looking+-- up a user from the database.+--+-- This function will:+--+-- 1. Check the password+--+-- 2. Login the user into the current session+--+-- 3. Mark success/failure of the authentication trial on the user record+checkPasswordAndLogin+  :: AuthUser               -- ^ An existing user, somehow looked up from db+  -> Password               -- ^ A ClearText password+  -> Handler b (AuthManager b) (Either AuthFailure AuthUser)+checkPasswordAndLogin u pw =+  case userLockedOutUntil u of+    Just x -> do+      now <- liftIO getCurrentTime+      if now > x+        then auth u+        else return . Left $ LockedOut x+    Nothing -> auth u+  where+    auth user =+      case authenticatePassword user pw of+        Just e -> do+          markAuthFail user+          return $ Left e+        Nothing -> do+          forceLogin user+          modify (\mgr -> mgr { activeUser = Just user })+          user' <- markAuthSuccess user+          return $ Right user'+++------------------------------------------------------------------------------+-- | Login and persist the given 'AuthUser' in the active session+--+-- Meant to be used if you have other means of being sure that the person is+-- who she says she is.+forceLogin+  :: AuthUser+  -- ^ An existing user, somehow looked up from db+  -> Handler b (AuthManager b) (Either AuthFailure AuthUser)+forceLogin u = do+  s <- gets session+  withSession s $ do+    case userId u of+      Just x -> do+        withTop s (setSessionUserId x)+        return $ Right u+      Nothing -> return . Left $+        AuthError "forceLogin: Can't force the login of a user without userId"+++------------------------------------------------------------------------------+-- Internal, non-exported helpers+--+------------------------------------------------------------------------------+++getRememberToken :: (Serialize t, MonadSnap m)+                 => Key+                 -> ByteString+                 -> Maybe Int+                 -> m (Maybe t)+getRememberToken sk rc rp = getSecureCookie rc sk rp+++setRememberToken :: (Serialize t, MonadSnap m)+                 => Key+                 -> ByteString+                 -> Maybe Int+                 -> t+                 -> m ()+setRememberToken sk rc rp token = setSecureCookie rc sk rp token+++forgetRememberToken :: MonadSnap m => ByteString -> m ()+forgetRememberToken rc = expireCookie rc (Just "/")+++------------------------------------------------------------------------------+-- | Set the current user's 'UserId' in the active session+setSessionUserId :: UserId -> Handler b SessionManager ()+setSessionUserId (UserId t) = setInSession "__user_id" t+++------------------------------------------------------------------------------+-- | Remove 'UserId' from active session, effectively logging the user out.+removeSessionUserId :: Handler b SessionManager ()+removeSessionUserId = deleteFromSession "__user_id"+++------------------------------------------------------------------------------+-- | Get the current user's 'UserId' from the active session+getSessionUserId :: Handler b SessionManager (Maybe UserId)+getSessionUserId = do+  uid <- getFromSession "__user_id"+  return $ uid >>= return . UserId+++------------------------------------------------------------------------------+-- | Check password for a given user.+--+-- Returns "Nothing" if check is successful and an "IncorrectPassword" error+-- otherwise+authenticatePassword+  :: AuthUser        -- ^ Looked up from the back-end+  -> Password        -- ^ Check against this password+  -> Maybe AuthFailure+authenticatePassword u pw = auth+  where+    auth = case userPassword u of+      Nothing -> Just PasswordMissing+      Just upw -> check $ checkPassword pw upw+    check b = if b then Nothing else Just IncorrectPassword+++------------------------------------------------------------------------------+-- | Wrap lookups around request-local cache+cacheOrLookup+  :: Handler b (AuthManager b) (Maybe AuthUser)+  -- ^ Lookup action to perform if request local cache is empty+  -> Handler b (AuthManager b) (Maybe AuthUser)+cacheOrLookup f = do+  au <- gets activeUser+  if isJust au+    then return au+    else do+      au' <- f+      modify (\mgr -> mgr { activeUser = au' })+      return au'+++------------------------------------------------------------------------------+-- | Register a new user by specifying login and password 'Param' fields+registerUser+  :: ByteString -- Login field+  -> ByteString -- Password field+  -> Handler b (AuthManager b) AuthUser+registerUser lf pf = do+  l <- fmap decodeUtf8 `fmap` getParam lf+  p <- getParam pf+  case liftM2 (,) l p of+    Nothing -> throw PasswordMissing+    Just (lgn, pwd) -> do+      createUser lgn pwd+++------------------------------------------------------------------------------+-- | A 'MonadSnap' handler that processes a login form.+--+-- The request paremeters are passed to 'performLogin'+loginUser+  :: ByteString+  -- ^ Username field+  -> ByteString+  -- ^ Password field+  -> Maybe ByteString+  -- ^ Remember field; Nothing if you want no remember function.+  -> (AuthFailure -> Handler b (AuthManager b) ())+  -- ^ Upon failure+  -> Handler b (AuthManager b) ()+  -- ^ Upon success+  -> Handler b (AuthManager b) ()+loginUser unf pwdf remf loginFail loginSucc = do+    username <- getParam unf+    password <- getParam pwdf+    remember <- maybe False (=="1") `fmap`+                maybe (return Nothing) getParam remf+    mMatch <- case password of+      Nothing -> return $ Left PasswordMissing+      Just password' -> do+        case username of+          Nothing -> return . Left $ AuthError "Username is missing"+          Just username' -> do+            loginByUsername username' (ClearText password') remember+    either loginFail (const loginSucc) mMatch+++------------------------------------------------------------------------------+-- | Simple handler to log the user out. Deletes user from session.+logoutUser+  :: Handler b (AuthManager b) ()+  -- ^ What to do after logging out+  -> Handler b (AuthManager b) ()+logoutUser target = logout >> target+++------------------------------------------------------------------------------+-- | Require that an authenticated 'AuthUser' is present in the current+-- session.+--+-- This function has no DB cost - only checks to see if a user_id is present+-- in the current session.+requireUser+  :: Lens b (Snaplet (AuthManager b))+  -- Lens reference to an "AuthManager"+  -> Handler b v a+  -- ^ Do this if no authenticated user is present.+  -> Handler b v a+  -- ^ Do this if an authenticated user is present.+  -> Handler b v a+requireUser auth bad good = do+  loggedIn <- withTop auth isLoggedIn+  if loggedIn then good else bad+++------------------------------------------------------------------------------+-- | Run a function on the backend, and return the result.+--+-- This uses an existential type so that the backend type doesn't+-- 'escape' AuthManager.  The reason that the type is Handler b+-- (AuthManager v) a and not a is because anything that uses the+-- backend will return an IO something, which you can liftIO, or a+-- Handler b (AuthManager v) a if it uses other handler things.+withBackend+  :: (forall r. (IAuthBackend r) => r -> Handler b (AuthManager v) a)+  -- ^ The function to run with the handler.+  -> Handler b (AuthManager v) a+withBackend f = join $ do+  (AuthManager bckend _ _ _ _ _ _ _) <- get+  return $ f bckend
+ src/Snap/Snaplet/Auth/SpliceHelpers.hs view
@@ -0,0 +1,75 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}++{-|++  Some pre-packaged splices that add convenience to a Heist-enabled+  application.++-}++module Snap.Snaplet.Auth.SpliceHelpers+  (+    addAuthSplices+  , ifLoggedIn+  , ifLoggedOut+  ) where++import           Data.Lens.Lazy+import qualified Text.XmlHtml as X+import           Text.Templating.Heist++import           Snap.Snaplet+import           Snap.Snaplet.Auth.AuthManager+import           Snap.Snaplet.Auth.Handlers+import           Snap.Snaplet.Heist+++------------------------------------------------------------------------------+-- | Add all standard auth splices to a Heist-enabled application.+--+-- This adds the following splices:+-- \<ifLoggedIn\>+-- \<ifLoggedOut\>+addAuthSplices+  :: HasHeist b+  => Lens b (Snaplet (AuthManager b))+  -- ^ A lens reference to 'AuthManager'+  -> Initializer b v ()+addAuthSplices auth = addSplices+  [ ("ifLoggedIn", ifLoggedIn auth)+  , ("ifLoggedOut", ifLoggedOut auth)+  ]+++------------------------------------------------------------------------------+-- | A splice that can be used to check for existence of a user. If a user is+-- present, this will run the contents of the node.+--+-- > <ifLoggedIn> Show this when there is a logged in user </ifLoggedIn>+ifLoggedIn+  :: Lens b (Snaplet (AuthManager b))+  -> SnapletSplice b v+ifLoggedIn auth = do+  chk <- liftHandler $ withTop auth isLoggedIn+  case chk of+    True -> liftHeist $ getParamNode >>= return . X.childNodes+    False -> return []+++------------------------------------------------------------------------------+-- | A splice that can be used to check for absence of a user. If a user is+-- not present, this will run the contents of the node.+--+-- > <ifLoggedOut> Show this when there is a logged in user </ifLoggedOut>+ifLoggedOut+  :: Lens b (Snaplet (AuthManager b))+  -> SnapletSplice b v+ifLoggedOut auth = do+  chk <- liftHandler $ withTop auth isLoggedIn+  case chk of+    False -> liftHeist $ getParamNode >>= return . X.childNodes+    True -> return []+
+ src/Snap/Snaplet/Auth/Types.hs view
@@ -0,0 +1,175 @@+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}++module Snap.Snaplet.Auth.Types where++import           Control.Monad.CatchIO+import           Data.Aeson+import           Data.ByteString (ByteString)+import           Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as HM+import           Data.Hashable (Hashable)+import           Data.Time+import           Data.Typeable+import           Data.Text (Text)+import           Crypto.PasswordStore+++------------------------------------------------------------------------------+-- | Password is clear when supplied by the user and encrypted later when+-- returned from the db.+data Password = ClearText ByteString+              | Encrypted ByteString+              deriving (Read, Show, Ord, Eq)+++------------------------------------------------------------------------------+-- Turn a 'ClearText' password into an 'Encrypted' password, ready to be+-- stuffed into a database.+encryptPassword :: Password -> IO Password+encryptPassword p@(Encrypted {}) = return p+encryptPassword (ClearText p) = do+  hashed <- makePassword p 12+  return $ Encrypted hashed+++checkPassword :: Password -> Password -> Bool+checkPassword (ClearText pw) (Encrypted pw') = verifyPassword pw pw'+checkPassword _ _ =+  error "checkPassword failed. Make sure you pass ClearText passwords"+++------------------------------------------------------------------------------+-- | Authentication failures indicate what went wrong during authentication.+-- They may provide useful information to the developer, although it is+-- generally not advisable to show the user the exact details about why login+-- failed.+data AuthFailure =+    UserNotFound+  | IncorrectPassword+  | PasswordMissing+  | LockedOut UTCTime+  -- ^ Locked out until given time+  | AuthError String+  deriving (Read, Show, Ord, Eq, Typeable)+++instance Exception AuthFailure+++------------------------------------------------------------------------------+-- | Internal representation of a 'User'. By convention, we demand that the+-- application is able to directly fetch a 'User' using this identifier.+--+-- Think of this type as a secure, authenticated user. You should normally+-- never see this type unless a user has been authenticated.+newtype UserId = UserId { unUid :: Text }+    deriving (Read,Show,Ord,Eq,FromJSON,ToJSON,Hashable)+++-- | This will be replaced by a role-based permission system.+data Role = Role ByteString+  deriving (Read,Show,Ord,Eq)+++------------------------------------------------------------------------------+-- | Type representing the concept of a User in your application.+data AuthUser = AuthUser+  { userId :: Maybe UserId+  , userLogin :: Text+  , userPassword :: Maybe Password+  , userActivatedAt :: Maybe UTCTime+  , userSuspendedAt :: Maybe UTCTime+  , userRememberToken :: Maybe Text+  , userLoginCount :: Int+  , userFailedLoginCount :: Int+  , userLockedOutUntil :: Maybe UTCTime+  , userCurrentLoginAt :: Maybe UTCTime+  , userLastLoginAt :: Maybe UTCTime+  , userCurrentLoginIp :: Maybe ByteString+  , userLastLoginIp :: Maybe ByteString+  , userCreatedAt :: Maybe UTCTime+  , userUpdatedAt :: Maybe UTCTime+  , userRoles :: [Role]+  , userMeta :: HashMap Text Value+  } deriving (Show,Eq)+++------------------------------------------------------------------------------+-- | Default AuthUser that has all empty values.+defAuthUser :: AuthUser+defAuthUser = AuthUser {+    userId = Nothing+  , userLogin = ""+  , userPassword = Nothing+  , userActivatedAt = Nothing+  , userSuspendedAt = Nothing+  , userRememberToken = Nothing+  , userLoginCount = 0+  , userFailedLoginCount = 0+  , userLockedOutUntil = Nothing+  , userCurrentLoginAt = Nothing+  , userLastLoginAt = Nothing+  , userCurrentLoginIp = Nothing+  , userLastLoginIp = Nothing+  , userCreatedAt = Nothing+  , userUpdatedAt = Nothing+  , userRoles = []+  , userMeta = HM.empty+}+++------------------------------------------------------------------------------+-- | Set a new password for the given user. Given password should be+-- clear-text; it will be encrypted into a 'Encrypted'.+setPassword :: AuthUser -> ByteString -> IO AuthUser+setPassword au pass = do+  pw <- Encrypted `fmap` (makePassword pass 12)+  return $ au { userPassword = Just pw }+++------------------------------------------------------------------------------+-- | Authetication settings defined at initialization time+data AuthSettings = AuthSettings {+    asMinPasswdLen :: Int+  -- ^ Currently not used/checked+  , asRememberCookieName :: ByteString+  -- ^ Name of the desired remember cookie+  , asRememberPeriod :: Maybe Int+  -- ^ How long to remember when the option is used in rest of the API.+  -- 'Nothing' means remember until end of session.+  , asLockout :: Maybe (Int, NominalDiffTime)+  -- ^ Lockout strategy: ([MaxAttempts], [LockoutDuration])+  , asSiteKey :: FilePath+  -- ^ Location of app's encryption key+}+++------------------------------------------------------------------------------+-- | Default settings for Auth.+--+-- > asMinPasswdLen = 8+-- > asRememberCookieName = "_remember"+-- > asRememberPeriod = Just (2*7*24*60*60) = 2 weeks+-- > asLockout = Nothing+-- > asSiteKey = "site_key.txt"+defAuthSettings :: AuthSettings+defAuthSettings = AuthSettings {+    asMinPasswdLen = 8+  , asRememberCookieName = "_remember"+  , asRememberPeriod = Just (2*7*24*60*60)+  , asLockout = Nothing+  , asSiteKey = "site_key.txt"+}+++data BackendError =+    DuplicateLogin+  | BackendError String+  deriving (Eq,Show,Read,Typeable)+++instance Exception BackendError+
+ src/Snap/Snaplet/Heist.hs view
@@ -0,0 +1,201 @@+{-|++The Heist snaplet makes it easy to add Heist to your application and use it in+other snaplets.++-}++module Snap.Snaplet.Heist+  (+  -- * Heist and its type class+    Heist+  , HasHeist(..)++  -- * Initializer Functions+  -- $initializerSection+  , heistInit+  , addTemplates+  , addTemplatesAt+  , modifyHeistTS+  , withHeistTS+  , addSplices++  -- * Handler Functions+  -- $handlerSection+  , render+  , renderAs+  , heistServe+  , heistServeSingle+  , heistLocal+  , withSplices+  , renderWithSplices++  -- * Writing Splices+  -- $spliceSection+  , Unclassed.SnapletHeist+  , Unclassed.SnapletSplice+  , Unclassed.liftHeist+  , Unclassed.liftHandler+  , Unclassed.liftAppHandler+  , Unclassed.liftWith+  , Unclassed.bindSnapletSplices++  , clearHeistCache+  ) where++import           Prelude hiding (id, (.))+import           Data.ByteString (ByteString)+import           Data.Lens.Lazy+import           Data.Text (Text)+import           Text.Templating.Heist++import           Snap.Snaplet++import qualified Snap.Snaplet.HeistNoClass as Unclassed+import           Snap.Snaplet.HeistNoClass (Heist, heistInit, clearHeistCache)+++------------------------------------------------------------------------------+-- | A single snaplet should never need more than one instance of Heist as a+-- subsnaplet.  This type class allows you to make it easy for other snaplets+-- to get the lens that identifies the heist snaplet.  Here's an example of+-- how the heist snaplet might be declared:+--+-- > data App = App { _heist :: Snaplet (Heist App) }+-- > mkLabels [''App]+-- >+-- > instance HasHeist App where heistLens = subSnaplet heist+-- >+-- > appInit = makeSnaplet "app" "" Nothing $ do+-- >     h <- nestSnaplet "heist" $ heistInit "templates"+-- >     addSplices myAppSplices+-- >     return $ App h+class HasHeist b where+    -- | A lens to the Heist snaplet.  The b parameter to Heist will+    -- typically be the base state of your application.+    heistLens :: Lens (Snaplet b) (Snaplet (Heist b))+++-- $initializerSection+-- This section contains functions for use in setting up your Heist state+-- during initialization.+++------------------------------------------------------------------------------+-- | Adds templates to the Heist TemplateState.  Other snaplets should use+-- this function to add their own templates.  The templates are automatically+-- read from the templates directory in the current snaplet's filesystem root.+addTemplates :: HasHeist b => ByteString -> Initializer b v ()+addTemplates pfx = withTop' heistLens (Unclassed.addTemplates pfx)+++------------------------------------------------------------------------------+-- | Adds templates to the Heist TemplateState, and lets you specify where+-- they are found in the filesystem.+addTemplatesAt :: HasHeist b => ByteString -> FilePath -> Initializer b v ()+addTemplatesAt pfx p = withTop' heistLens (Unclassed.addTemplatesAt pfx p)+++------------------------------------------------------------------------------+-- | Allows snaplets to add splices.+addSplices :: (HasHeist b)+           => [(Text, Unclassed.SnapletSplice b v)] -> Initializer b v ()+addSplices = Unclassed.addSplices' heistLens+++------------------------------------------------------------------------------+-- | More general function allowing arbitrary TemplateState modification.+-- Without this function you wouldn't be able to bind more complicated splices+-- like the cache tag.+modifyHeistTS :: (HasHeist b)+              => (TemplateState (Handler b b) -> TemplateState (Handler b b))+              -> Initializer b v ()+modifyHeistTS = Unclassed.modifyHeistTS' heistLens+++------------------------------------------------------------------------------+-- | Runs a function on with the Heist snaplet's 'TemplateState'.+withHeistTS :: (HasHeist b)+            => (TemplateState (Handler b b) -> a)+            -> Handler b v a+withHeistTS = Unclassed.withHeistTS' heistLens+++-- $handlerSection+-- This section contains functions in the 'Handler' monad that you'll use in+-- processing requests.+++------------------------------------------------------------------------------+-- | Renders a template as text\/html. If the given template is not found,+-- this returns 'empty'.+render :: HasHeist b => ByteString -> Handler b v ()+render t = withTop' heistLens (Unclassed.render t)+++------------------------------------------------------------------------------+-- | Renders a template as the given content type.  If the given template+-- is not found, this returns 'empty'.+renderAs :: HasHeist b => ByteString -> ByteString -> Handler b v ()+renderAs ct t = withTop' heistLens (Unclassed.renderAs ct t)+++------------------------------------------------------------------------------+-- | Analogous to 'fileServe'. If the template specified in the request path+-- is not found, it returns 'empty'.+heistServe :: HasHeist b => Handler b v ()+heistServe = withTop' heistLens Unclassed.heistServe+++------------------------------------------------------------------------------+-- | Analogous to 'fileServeSingle'. If the given template is not found,+-- this throws an error.+heistServeSingle :: HasHeist b => ByteString -> Handler b v ()+heistServeSingle t = withTop' heistLens (Unclassed.heistServeSingle t)+++------------------------------------------------------------------------------+-- | Renders a template with a given set of splices.  This is syntax sugar for+-- a common combination of heistLocal, bindSplices, and render.+renderWithSplices :: HasHeist b+                  => ByteString+                  -> [(Text, Unclassed.SnapletSplice b v)]+                  -> Handler b v ()+renderWithSplices = Unclassed.renderWithSplices' heistLens+++------------------------------------------------------------------------------+-- | Runs an action with additional splices bound into the Heist+-- 'TemplateState'.+withSplices :: HasHeist b+            => [(Text, Unclassed.SnapletSplice b v)]+            -> Handler b v a+            -> Handler b v a+withSplices = Unclassed.withSplices' heistLens+++------------------------------------------------------------------------------+-- | Runs a handler with a modified 'TemplateState'.  You might want to use+-- this if you had a set of splices which were customised for a specific+-- action.  To do that you would do:+--+-- > heistLocal (bindSplices mySplices) handlerThatNeedsSplices+heistLocal :: HasHeist b+           => (TemplateState (Handler b b) -> TemplateState (Handler b b))+           -> Handler b v a+           -> Handler b v a+heistLocal = Unclassed.heistLocal' heistLens+++-- $spliceSection+-- As can be seen in the type signature of heistLocal, the internal+-- TemplateState used by the heist snaplet is parameterized by (Handler b b).+-- The reasons for this are beyond the scope of this discussion, but the+-- result is that 'lift' inside a splice only works with @Handler b b@+-- actions.  When you're writing your own snaplets you obviously would rather+-- work with @Handler b v@ so your local snaplet's state is available.  We+-- provide the SnapletHeist monad to make this possible.  The general rule is+-- that when you're using Snaplets and Heist, use SnapletHeist instead of+-- HeistT (previously called TemplateMonad) and use SnapletSplice instead of+-- Splice.+
+ src/Snap/Snaplet/HeistNoClass.hs view
@@ -0,0 +1,384 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings          #-}+{-# LANGUAGE MultiParamTypeClasses      #-}+{-# LANGUAGE FlexibleInstances          #-}+{-# LANGUAGE FlexibleContexts           #-}+{-# LANGUAGE TypeSynonymInstances       #-}+module Snap.Snaplet.HeistNoClass+  ( Heist+  , heistInit+  , clearHeistCache++  , addTemplates+  , addTemplatesAt+  , modifyHeistTS+  , modifyHeistTS'+  , withHeistTS+  , withHeistTS'+  , addSplices+  , addSplices'+  , render+  , renderAs+  , heistServe+  , heistServeSingle+  , heistLocal+  , withSplices+  , renderWithSplices+  , heistLocal'+  , withSplices'+  , renderWithSplices'++  , SnapletHeist+  , SnapletSplice+  , runSnapletSplice+  , liftHeist+  , liftWith+  , liftHandler+  , liftAppHandler+  , bindSnapletSplices+  ) where++import           Prelude hiding ((.), id)+import           Control.Arrow+import           Control.Applicative+import           Control.Category+import           Control.Monad.CatchIO (MonadCatchIO)+import           Control.Monad.Reader+import           Control.Monad.State+import           Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as B+import qualified Data.ByteString.UTF8 as U+import           Data.Maybe+import           Data.Monoid+import           Data.Lens.Lazy+import           Data.Text (Text)+import qualified Data.Text as T+import           System.FilePath.Posix+import           Text.Templating.Heist+import           Text.Templating.Heist.Splices.Cache++import           Snap.Snaplet+import           Snap.Core+import           Snap.Util.FileServe+++------------------------------------------------------------------------------+-- | The state for the Heist snaplet.  To use the Heist snaplet in your app+-- include this in your application state and use 'heistInit' to initialize+-- it.  The type parameter b will typically be the base state type for your+-- application.+data Heist b = Heist+    { _heistTS       :: TemplateState (Handler b b)+    , _heistCTS      :: CacheTagState+    }+++------------------------------------------------------------------------------+changeTS :: (TemplateState (Handler a a) -> TemplateState (Handler a a))+         -> Heist a+         -> Heist a+changeTS f (Heist ts cts) = Heist (f ts) cts+++------------------------------------------------------------------------------+-- | Clears data stored by the cache tag.  The cache tag automatically reloads+-- its data when the specified TTL expires, but sometimes you may want to+-- trigger a manual reload.  This function lets you do that.+clearHeistCache :: Heist b -> IO ()+clearHeistCache = clearCacheTagState . _heistCTS+++------------------------------------------------------------------------------+-- SnapletSplice functions+------------------------------------------------------------------------------+++------------------------------------------------------------------------------+-- | This instance is here because we don't want the heist package to depend+-- on anything from snap packages.+instance MonadSnap m => MonadSnap (HeistT m) where+    liftSnap = lift . liftSnap+++------------------------------------------------------------------------------+-- | Monad for working with Heist's API from within a snaplet.+newtype SnapletHeist b v a = SnapletHeist+    (ReaderT (Lens (Snaplet b) (Snaplet v)) (HeistT (Handler b b)) a)+  deriving ( Monad+           , Functor+           , Applicative+           , Alternative+           , MonadIO+           , MonadPlus+           , MonadReader (Lens (Snaplet b) (Snaplet v))+           , MonadCatchIO+           , MonadSnap+           )+++------------------------------------------------------------------------------+-- | Type alias for convenience.+type SnapletSplice b v = SnapletHeist b v Template+++------------------------------------------------------------------------------+-- | Runs the SnapletSplice.+runSnapletSplice :: (Lens (Snaplet b) (Snaplet v))+                 -> SnapletHeist b v a+                 -> HeistT (Handler b b) a+runSnapletSplice l (SnapletHeist m) = runReaderT m l+++withSS :: (Lens (Snaplet b) (Snaplet v) -> Lens (Snaplet b) (Snaplet v'))+       -> SnapletHeist b v' a+       -> SnapletHeist b v a+withSS f (SnapletHeist m) = SnapletHeist $ withReaderT f m+++------------------------------------------------------------------------------+-- | Lifts a HeistT action into SnapletHeist.  Use this with all the functions+-- from the Heist API.+liftHeist :: HeistT (Handler b b) a -> SnapletHeist b v a+liftHeist = SnapletHeist . lift+++------------------------------------------------------------------------------+-- | Common idiom for the combination of liftHandler and withTop.+liftWith :: (Lens (Snaplet b) (Snaplet v'))+         -> Handler b v' a+         -> SnapletHeist b v a+liftWith l = liftHeist . lift . withTop' l+++------------------------------------------------------------------------------+-- | Lifts a Handler into SnapletHeist.+liftHandler :: Handler b v a -> SnapletHeist b v a+liftHandler m = do+    l <- ask+    liftWith l m+++------------------------------------------------------------------------------+-- | Lifts a (Handler b b) into SnapletHeist.+liftAppHandler :: Handler b b a -> SnapletHeist b v a+liftAppHandler = liftHeist . lift+++instance MonadState v (SnapletHeist b v) where+    get = do+        l <- ask+        b <- liftAppHandler getSnapletState+        return $ getL (snapletValue . l) b+    put s = do+        l <- ask+        b <- liftAppHandler getSnapletState+        liftAppHandler $ putSnapletState $ setL (snapletValue . l) s b+++------------------------------------------------------------------------------+-- | MonadSnaplet instance gives us access to the snaplet infrastructure.+instance MonadSnaplet SnapletHeist where+    getLens = ask+    with' l = withSS (l .)+    withTop' l = withSS (const id) . with' l+    getOpaqueConfig = do+        l <- ask+        b <- liftAppHandler getSnapletState+        return $ getL (snapletConfig . l) b+++------------------------------------------------------------------------------+-- | SnapletSplices version of bindSplices.+bindSnapletSplices :: (Lens (Snaplet b) (Snaplet v))+                   -> [(Text, SnapletSplice b v)]+                   -> TemplateState (Handler b b)+                   -> TemplateState (Handler b b)+bindSnapletSplices l splices =+    bindSplices $ map (second $ runSnapletSplice l) splices+++------------------------------------------------------------------------------+-- Initializer functions+------------------------------------------------------------------------------+++------------------------------------------------------------------------------+-- | The 'Initializer' for 'Heist'.+heistInit :: FilePath+          -> SnapletInit b (Heist b)+heistInit templateDir =+    makeSnaplet "heist" "" Nothing $ do+        (cacheFunc, cts) <- liftIO mkCacheTag+        let origTs = cacheFunc emptyTemplateState+        ts <- liftIO $ loadTemplates templateDir origTs >>=+                       either error return+        addRoutes [ ("", heistServe) ]+        printInfo $ T.pack $ unwords+            [ "...loaded"+            , (show $ length $ templateNames ts)+            , "templates"+            ]++        return $ Heist ts cts+++addTemplates :: ByteString -> Initializer b (Heist b) ()+addTemplates urlPrefix = do+    snapletPath <- getSnapletFilePath+    addTemplatesAt urlPrefix (snapletPath </> "templates")+++addTemplatesAt :: ByteString+               -> FilePath+               -> Initializer b (Heist b) ()+addTemplatesAt urlPrefix templateDir = do+    ts <- liftIO $ loadTemplates templateDir emptyTemplateState+                   >>= either error return+    printInfo $ T.pack $ unwords+        [ "...adding"+        , (show $ length $ templateNames ts)+        , "templates from"+        , templateDir+        , "with route prefix"+        , (U.toString urlPrefix) ++ "/"+        ]+    addPostInitHook $ return . changeTS+        (`mappend` addTemplatePathPrefix urlPrefix ts)+++modifyHeistTS' :: (Lens (Snaplet b) (Snaplet (Heist b)))+               -> (TemplateState (Handler b b) -> TemplateState (Handler b b))+               -> Initializer b v ()+modifyHeistTS' heist f = do+    _lens <- getLens+    withTop' heist $ addPostInitHook $ return . changeTS f+++modifyHeistTS :: (Lens b (Snaplet (Heist b)))+              -> (TemplateState (Handler b b) -> TemplateState (Handler b b))+              -> Initializer b v ()+modifyHeistTS heist f = modifyHeistTS' (subSnaplet heist) f+++withHeistTS' :: (Lens (Snaplet b) (Snaplet (Heist b)))+             -> (TemplateState (Handler b b) -> a)+             -> Handler b v a+withHeistTS' heist f = withTop' heist $ gets (f . _heistTS)+++withHeistTS :: (Lens b (Snaplet (Heist b)))+            -> (TemplateState (Handler b b) -> a)+            -> Handler b v a+withHeistTS heist f = withHeistTS' (subSnaplet heist) f+++addSplices' :: (Lens (Snaplet b) (Snaplet (Heist b)))+            -> [(Text, SnapletSplice b v)]+            -> Initializer b v ()+addSplices' heist splices = do+    _lens <- getLens+    withTop' heist $ addPostInitHook $+        return . changeTS (bindSnapletSplices _lens splices)+++addSplices :: (Lens b (Snaplet (Heist b)))+           -> [(Text, SnapletSplice b v)]+           -> Initializer b v ()+addSplices heist splices = addSplices' (subSnaplet heist) splices+++------------------------------------------------------------------------------+-- Handler functions+------------------------------------------------------------------------------+++------------------------------------------------------------------------------+-- | Internal helper function for rendering.+renderHelper :: Maybe MIMEType+             -> ByteString+             -> Handler b (Heist b) ()+renderHelper c t = do+    (Heist ts _) <- get+    withTop' id $ renderTemplate ts t >>= maybe pass serve+  where+    serve (b, mime) = do+        modifyResponse $ setContentType $ fromMaybe mime c+        writeBuilder b+++render :: ByteString+       -- ^ Name of the template+       -> Handler b (Heist b) ()+render t = renderHelper Nothing t+++renderAs :: ByteString+         -- ^ Content type+         -> ByteString+         -- ^ Name of the template+         -> Handler b (Heist b) ()+renderAs ct t = renderHelper (Just ct) t+++heistServe :: Handler b (Heist b) ()+heistServe =+    ifTop (render "index") <|> (render . B.pack =<< getSafePath)+++heistServeSingle :: ByteString+                 -> Handler b (Heist b) ()+heistServeSingle t =+    render t <|> error ("Template " ++ show t ++ " not found.")+++heistLocal' :: (Lens (Snaplet b) (Snaplet (Heist b)))+            -> (TemplateState (Handler b b) -> TemplateState (Handler b b))+            -> Handler b v a+            -> Handler b v a+heistLocal' heist f m = do+    hs  <- withTop' heist get+    withTop' heist $ modify $ changeTS f+    res <- m+    withTop' heist $ put hs+    return res+++heistLocal :: (Lens b (Snaplet (Heist b)))+           -> (TemplateState (Handler b b) -> TemplateState (Handler b b))+           -> Handler b v a+           -> Handler b v a+heistLocal heist f m = heistLocal' (subSnaplet heist) f m+++withSplices' :: (Lens (Snaplet b) (Snaplet (Heist b)))+             -> [(Text, SnapletSplice b v)]+             -> Handler b v a+             -> Handler b v a+withSplices' heist splices m = do+    _lens <- getLens+    heistLocal' heist (bindSnapletSplices _lens splices) m+++withSplices :: (Lens b (Snaplet (Heist b)))+            -> [(Text, SnapletSplice b v)]+            -> Handler b v a+            -> Handler b v a+withSplices heist splices m = withSplices' (subSnaplet heist) splices m+++renderWithSplices' :: (Lens (Snaplet b) (Snaplet (Heist b)))+                   -> ByteString+                   -> [(Text, SnapletSplice b v)]+                   -> Handler b v ()+renderWithSplices' heist t splices =+    withSplices' heist splices $ withTop' heist $ render t+++renderWithSplices :: (Lens b (Snaplet (Heist b)))+                  -> ByteString+                  -> [(Text, SnapletSplice b v)]+                  -> Handler b v ()+renderWithSplices heist t splices =+    renderWithSplices' (subSnaplet heist) t splices++
+ src/Snap/Snaplet/Internal/Initializer.hs view
@@ -0,0 +1,489 @@+{-# LANGUAGE OverloadedStrings          #-}+{-# LANGUAGE BangPatterns               #-}++module Snap.Snaplet.Internal.Initializer+  ( addPostInitHook+  , addPostInitHookBase+  , toSnapletHook+  , bracketInit+  , modifyCfg+  , nestSnaplet+  , embedSnaplet+  , makeSnaplet+  , nameSnaplet+  , onUnload+  , addRoutes+  , wrapHandlers+  , runInitializer+  , runSnaplet+  , combineConfig+  , serveSnaplet+  , printInfo+  ) where++import           Prelude hiding ((.), id, catch)+import           Control.Category+import           Control.Concurrent.MVar+import           Control.Exception (SomeException)+import           Control.Monad+import           Control.Monad.CatchIO hiding (Handler)+import           Control.Monad.Reader+import           Control.Monad.State+import           Control.Monad.Trans.Writer hiding (pass)+import           Data.ByteString.Char8 (ByteString)+import qualified Data.ByteString.Char8 as B+import           Data.Configurator+import           Data.IORef+import           Data.Maybe+import           Data.Lens.Lazy+import           Data.Text (Text)+import qualified Data.Text as T+import           Snap.Http.Server+import           Snap.Core+import           Snap.Util.GZip+import           System.Directory+import           System.Directory.Tree+import           System.FilePath.Posix+import           System.IO++import qualified Snap.Snaplet.Internal.LensT as LT+import qualified Snap.Snaplet.Internal.Lensed as L+import           Snap.Snaplet.Internal.Types+++------------------------------------------------------------------------------+-- | 'get' for InitializerState.+iGet :: Initializer b v (InitializerState b)+iGet = Initializer $ LT.getBase+++------------------------------------------------------------------------------+-- | 'modify' for InitializerState.+iModify :: (InitializerState b -> InitializerState b) -> Initializer b v ()+iModify f = Initializer $ do+    b <- LT.getBase+    LT.putBase $ f b+++------------------------------------------------------------------------------+-- | 'gets' for InitializerState.+iGets :: (InitializerState b -> a) -> Initializer b v a+iGets f = Initializer $ do+    b <- LT.getBase+    return $ f b+++------------------------------------------------------------------------------+-- | Converts a plain hook into a Snaplet hook.+toSnapletHook :: (v -> IO v) -> (Snaplet v -> IO (Snaplet v))+toSnapletHook f (Snaplet cfg val) = do+    val' <- f val+    return $! Snaplet cfg val'+++------------------------------------------------------------------------------+-- | Adds an IO action that modifies the current snaplet state to be run at+-- the end of initialization on the state that was created.  This makes it+-- easier to allow one snaplet's state to be modified by another snaplet's+-- initializer.  A good example of this is when a snaplet has templates that+-- define its views.  The Heist snaplet provides the 'addTemplates' function+-- which allows other snaplets to set up their own templates.  'addTemplates'+-- is implemented using this function.+addPostInitHook :: (v -> IO v) -> Initializer b v ()+addPostInitHook = addPostInitHook' . toSnapletHook+++addPostInitHook' :: (Snaplet v -> IO (Snaplet v)) -> Initializer b v ()+addPostInitHook' h = do+    h' <- upHook h+    addPostInitHookBase h'+++------------------------------------------------------------------------------+-- | Variant of addPostInitHook for when you have things wrapped in a Snaplet.+addPostInitHookBase :: (Snaplet b -> IO (Snaplet b))+                    -> Initializer b v ()+addPostInitHookBase = Initializer . lift . tell . Hook+++------------------------------------------------------------------------------+-- | Helper function for transforming hooks.+upHook :: (Snaplet v -> IO (Snaplet v))+       -> Initializer b v (Snaplet b -> IO (Snaplet b))+upHook h = Initializer $ do+    l <- ask+    return $ upHook' l h+++------------------------------------------------------------------------------+-- | Helper function for transforming hooks.+upHook' :: (Lens b a) -> (a -> IO a) -> b -> IO b+upHook' l h b = do+    v <- h (getL l b)+    return $ setL l v b+++------------------------------------------------------------------------------+-- | Modifies the Initializer's SnapletConfig.+modifyCfg :: (SnapletConfig -> SnapletConfig) -> Initializer b v ()+modifyCfg f = iModify $ modL curConfig $ \c -> f c+++------------------------------------------------------------------------------+-- | If a snaplet has a filesystem presence, this function creates and copies+-- the files if they dont' already exist.+setupFilesystem :: Maybe (IO FilePath)+                -- ^ The directory where the snaplet's reference files are+                -- stored.  Nothing if the snaplet doesn't come with any files+                -- that need to be installed.+                -> FilePath+                -- ^ Directory where the files should be copied.+                -> Initializer b v ()+setupFilesystem Nothing _ = return ()+setupFilesystem (Just getSnapletDataDir) targetDir = do+    exists <- liftIO $ doesDirectoryExist targetDir+    unless exists $ do+        printInfo "...setting up filesystem"+        liftIO $ createDirectoryIfMissing True targetDir+        srcDir <- liftIO getSnapletDataDir+        (_ :/ dTree) <- liftIO $ readDirectoryWith B.readFile srcDir+        let (topDir,snapletId) = splitFileName targetDir+        _ <- liftIO $ writeDirectoryWith B.writeFile+               (topDir :/ dTree { name = snapletId })+        return ()+++------------------------------------------------------------------------------+-- | All snaplet initializers must be wrapped in a call to @makeSnaplet@,+-- which handles standardized housekeeping common to all snaplets.+-- Common usage will look something like+-- this:+--+-- @+-- fooInit :: SnapletInit b Foo+-- fooInit = makeSnaplet \"foo\" \"An example snaplet\" Nothing $ do+--     -- Your initializer code here+--     return $ Foo 42+-- @+--+-- Note that you're writing your initializer code in the Initializer monad,+-- and makeSnaplet converts it into an opaque SnapletInit type.  This allows+-- us to use the type system to ensure that the API is used correctly.+makeSnaplet :: Text+       -- ^ A default id for this snaplet.  This is only used when the+       -- end-user has not already set an id using the nameSnaplet function.+       -> Text+       -- ^ A human readable description of this snaplet.+       -> Maybe (IO FilePath)+       -- ^ The path to the directory holding the snaplet's reference+       -- filesystem content.  This will almost always be the directory+       -- returned by Cabal's getDataDir command, but it has to be passed in+       -- because it is defined in a package-specific import.  Setting this+       -- value to Nothing doesn't preclude the snaplet from having files in+       -- in the filesystem, it just means that they won't be copied there+       -- automatically.+       -> Initializer b v v+       -- ^ Snaplet initializer.+       -> SnapletInit b v+makeSnaplet snapletId desc getSnapletDataDir m = SnapletInit $ do+    modifyCfg $ \c -> if isNothing $ _scId c+        then setL scId (Just snapletId) c else c+    sid <- iGets (T.unpack . fromJust . _scId . _curConfig)+    topLevel <- iGets _isTopLevel+    unless topLevel $ modifyCfg $ \c -> setL scFilePath+        (_scFilePath c </> "snaplets" </> sid) c+    iModify (setL isTopLevel False)+    modifyCfg $ modL scUserConfig (subconfig (T.pack sid))+    modifyCfg $ setL scDescription desc+    cfg <- iGets _curConfig+    printInfo $ T.pack $ concat+      ["Initializing "+      ,sid+      ," @ /"+      ,B.unpack $ buildPath $ _scRouteContext cfg+      ]++    -- This has to happen here because it needs to be after scFilePath is set+    -- up but before snaplet.cfg is read.+    setupFilesystem getSnapletDataDir (_scFilePath cfg)++    liftIO $ addToConfig [Optional (_scFilePath cfg </> "snaplet.cfg")]+                         (_scUserConfig cfg)+    mkSnaplet m+++------------------------------------------------------------------------------+-- | Internal function that gets the SnapletConfig out of the initializer+-- state and uses it to create a (Snaplet a).+mkSnaplet :: Initializer b v a -> Initializer b v (Snaplet a)+mkSnaplet m = do+    res <- m+    cfg <- iGets _curConfig+    return $ Snaplet cfg res+++------------------------------------------------------------------------------+-- | Brackets an initializer computation, restoring curConfig after the+-- computation returns.+bracketInit :: Initializer b v a -> Initializer b v a+bracketInit m = do+    s <- iGet+    res <- m+    iModify (setL curConfig (_curConfig s))+    return res+++------------------------------------------------------------------------------+-- | Handles modifications to InitializerState that need to happen before a+-- snaplet is called with either nestSnaplet or embedSnaplet.+setupSnapletCall :: ByteString -> Initializer b v ()+setupSnapletCall rte = do+    curId <- iGets (fromJust . _scId . _curConfig)+    modifyCfg (modL scAncestry (curId:))+    modifyCfg (modL scId (const Nothing))+    unless (B.null rte) $ modifyCfg (modL scRouteContext (rte:))+++------------------------------------------------------------------------------+-- | Runs another snaplet's initializer and returns the initialized Snaplet+-- value.  Calling an initializer with nestSnaplet gives the nested snaplet+-- access to the same base state that the current snaplet has.  This makes it+-- possible for the child snaplet to make use of functionality provided by+-- sibling snaplets.+nestSnaplet :: ByteString+            -- ^ The root url for all the snaplet's routes.  An empty string+            -- gives the routes the same root as the parent snaplet's routes.+            -> (Lens v (Snaplet v1))+            -- ^ Lens identifying the snaplet+            -> SnapletInit b v1+            -- ^ The initializer function for the subsnaplet.+            -> Initializer b v (Snaplet v1)+nestSnaplet rte l (SnapletInit snaplet) = with l $ bracketInit $ do+    setupSnapletCall rte+    snaplet+++------------------------------------------------------------------------------+-- | Runs another snaplet's initializer and returns the initialized Snaplet+-- value.  The difference between this and nestSnaplet is the first type+-- parameter in the third argument.  The \"v1 v1\" makes the child snaplet+-- think that it is top-level, which means that it will not be able to use+-- functionality provided by snaplets included above it in the snaplet tree.+-- This strongly isolates the child snaplet, and allows you to eliminate the b+-- type variable.  The embedded snaplet can still get functionality from other+-- snaplets, but only if it nests or embeds the snaplet itself.+embedSnaplet :: ByteString+             -- ^ The root url for all the snaplet's routes.  An empty string+             -- gives the routes the same root as the parent snaplet's routes.+             --+             -- NOTE: Because of the stronger isolation provided by+             -- embedSnaplet, you should be more careful about using an empty+             -- string here.+             -> (Lens v (Snaplet v1))+             -- ^ Lens identifying the snaplet+             -> SnapletInit v1 v1+             -- ^ The initializer function for the subsnaplet.+             -> Initializer b v (Snaplet v1)+embedSnaplet rte l (SnapletInit snaplet) = bracketInit $ do+    curLens <- getLens+    setupSnapletCall rte+    chroot rte (subSnaplet l . curLens) snaplet+++------------------------------------------------------------------------------+-- | Changes the base state of an initializer.+chroot :: ByteString+       -> (Lens (Snaplet b) (Snaplet v1))+       -> Initializer v1 v1 a+       -> Initializer b v a+chroot rte l (Initializer m) = do+    curState <- iGet+    ((a,s), (Hook hook)) <- liftIO $ runWriterT $ LT.runLensT m id $+        curState {+          _handlers = [],+          _hFilter = id+        }+    let handler = chrootHandler l $ _hFilter s $ route $ _handlers s+    iModify $ modL handlers (++[(rte,handler)])+            . setL cleanup (_cleanup s)+    addPostInitHookBase $ upHook' l hook+    return a+++------------------------------------------------------------------------------+-- | Changes the base state of a handler.+chrootHandler :: (Lens (Snaplet v) (Snaplet b'))+              -> Handler b' b' a -> Handler b v a+chrootHandler l (Handler h) = Handler $ do+    s <- get+    (a, s') <- liftSnap $ L.runLensed h id (getL l s)+    modify $ setL l s'+    return a+++------------------------------------------------------------------------------+-- | Sets a snaplet's name.  All snaplets have a default name set by the+-- snaplet author.  This function allows you to override that name.  You will+-- have to do this if you have more than one instance of the same kind of+-- snaplet because snaplet names must be unique.  This function must+-- immediately surround the snaplet's initializer.  For example:+--+-- @fooState <- nestSnaplet \"fooA\" $ nameSnaplet \"myFoo\" $ fooInit@+nameSnaplet :: Text+            -- ^ The snaplet name+            -> SnapletInit b v+            -- ^ The snaplet initializer function+            -> SnapletInit b v+nameSnaplet nm (SnapletInit m) = SnapletInit $+    modifyCfg (setL scId (Just nm)) >> m+++------------------------------------------------------------------------------+-- | Adds routing to the current 'Handler'.  The new routes are merged with+-- the main routing section and take precedence over existing routing that was+-- previously defined.+addRoutes :: [(ByteString, Handler b v ())]+           -> Initializer b v ()+addRoutes rs = do+    l <- getLens+    ctx <- iGets (_scRouteContext . _curConfig)+    let rs' = map (\(r,h) -> (buildPath (r:ctx), withTop' l h)) rs+    iModify (\v -> modL handlers (++rs') v)+++------------------------------------------------------------------------------+-- | Wraps the snaplet's routing.  This can be used to provide a snaplet that+-- does per-request setup and cleanup, but then dispatches to the rest of the+-- application.+wrapHandlers :: (Handler b v () -> Handler b v ()) -> Initializer b v ()+wrapHandlers f0 = do+    f <- mungeFilter f0+    iModify (\v -> modL hFilter (f.) v)+++------------------------------------------------------------------------------+mungeFilter :: (Handler b v () -> Handler b v ())+            -> Initializer b v (Handler b b () -> Handler b b ())+mungeFilter f = do+    myLens <- Initializer ask+    return $ \m -> with' myLens $ f' m+  where+    f' (Handler m)       = f $ Handler $ L.withTop id m+++------------------------------------------------------------------------------+-- | Attaches an unload handler to the snaplet.  The unload handler will be+-- called when the server shuts down, or is reloaded.+onUnload :: IO () -> Initializer b v ()+onUnload m = iModify (\v -> modL cleanup (m>>) v)+++------------------------------------------------------------------------------+-- |+logInitMsg :: IORef Text -> Text -> IO ()+logInitMsg ref msg = atomicModifyIORef ref (\cur -> (cur `T.append` msg, ()))+++------------------------------------------------------------------------------+-- | Initializers should use this function for all informational or error+-- messages to be displayed to the user.  On application startup they will be+-- sent to the console.  When executed from the reloader, they will be sent+-- back to the user in the HTTP response.+printInfo :: Text -> Initializer b v ()+printInfo msg = do+    logRef <- iGets _initMessages+    liftIO $ logInitMsg logRef (msg `T.append` "\n")+++------------------------------------------------------------------------------+-- | Builds an IO reload action for storage in the SnapletState.+mkReloader :: MVar (Snaplet b)+           -> Initializer b b (Snaplet b)+           -> IO (Either String String)+mkReloader mvar i = do+    !res <- try $ runInitializer mvar i+    either bad good res+  where+    bad e = do+        return $ Left $ show (e :: SomeException)+    good (b,is) = do+        _ <- swapMVar mvar b+        msgs <- readIORef $ _initMessages is+        return $ Right $ T.unpack msgs+++------------------------------------------------------------------------------+-- | Runs a top-level snaplet in the Snap monad.+runBase :: Handler b b a+        -> MVar (Snaplet b)+        -> Snap a+runBase (Handler m) mvar = do+    !b <- liftIO (readMVar mvar)+    (!a, _) <- L.runLensed m id b+    return $! a+++------------------------------------------------------------------------------+-- |+runInitializer :: MVar (Snaplet b)+               -> Initializer b b (Snaplet b)+               -> IO (Snaplet b, InitializerState b)+runInitializer mvar b@(Initializer i) = do+    userConfig <- load [Optional "snaplet.cfg"]+    let builtinHandlers = [("/admin/reload", reloadSite)]+    let cfg = SnapletConfig [] "" Nothing "" userConfig [] (mkReloader mvar b)+    logRef <- newIORef ""+    ((res, s), (Hook hook)) <- runWriterT $ LT.runLensT i id $+        InitializerState True (return ()) builtinHandlers id cfg logRef+    res' <- hook res+    return (res', s)+++------------------------------------------------------------------------------+-- | Given a Snaplet initializer, produce the set of messages generated during+-- initialization, a snap handler, and a cleanup action.+runSnaplet :: SnapletInit b b -> IO (Text, Snap (), IO ())+runSnaplet (SnapletInit b) = do+    snapletMVar <- newEmptyMVar+    (siteSnaplet, is) <- runInitializer snapletMVar b+    putMVar snapletMVar siteSnaplet++    msgs <- liftIO $ readIORef $ _initMessages is+    let handler = runBase (_hFilter is $ route $ _handlers is) snapletMVar++    return (msgs, handler, _cleanup is)+++------------------------------------------------------------------------------+-- | Given a configuration and a snap handler, complete it and produce the+-- completed configuration as well as a new toplevel handler with things like+-- compression and a 500 handler set up.+combineConfig :: Config Snap a -> Snap () -> IO (Config Snap a, Snap ())+combineConfig config handler = do+    conf <- completeConfig config++    let catch500 = (flip catch $ fromJust $ getErrorHandler conf)+    let compress = if fromJust (getCompression conf)+                     then withCompression else id+    let site     = compress $ catch500 handler++    return (conf, site)+++------------------------------------------------------------------------------+-- | Serves a top-level snaplet as a web application. Reads command-line+-- arguments. FIXME: document this.+serveSnaplet :: Config Snap a -> SnapletInit b b -> IO ()+serveSnaplet startConfig initializer = do+    (msgs, handler, doCleanup) <- runSnaplet initializer++    config       <- commandLineConfig startConfig+    (conf, site) <- combineConfig config handler+    let serve = simpleHttpServe conf++    liftIO $ hPutStrLn stderr $ T.unpack msgs+    _ <- try $ serve $ site+         :: IO (Either SomeException ())+    doCleanup++
+ src/Snap/Snaplet/Internal/LensT.hs view
@@ -0,0 +1,105 @@+{-# LANGUAGE BangPatterns               #-}+{-# LANGUAGE FlexibleInstances          #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses      #-}++module Snap.Snaplet.Internal.LensT where++import           Control.Applicative+import           Control.Category+import           Control.Monad.CatchIO+import           Control.Monad.Reader+import           Control.Monad.State.Class+import           Data.Lens.Lazy+import           Prelude hiding ((.), id, catch)+import           Snap.Core++import           Snap.Snaplet.Internal.RST+++newtype LensT b v s m a = LensT (RST (Lens b v) s m a)+  deriving ( Monad+           , MonadTrans+           , Functor+           , Applicative+           , MonadIO+           , MonadPlus+           , MonadCatchIO+           , Alternative+           , MonadReader (Lens b v)+           , MonadSnap )+++------------------------------------------------------------------------------+instance (Monad m) => MonadState v (LensT b v b m) where+    get = lGet+    put = lPut+++------------------------------------------------------------------------------+getBase :: (Monad m) => LensT b v s m s+getBase = LensT get+{-# INLINE getBase #-}+++------------------------------------------------------------------------------+putBase :: (Monad m) => s -> LensT b v s m ()+putBase = LensT . put+{-# INLINE putBase #-}+++------------------------------------------------------------------------------+lGet :: (Monad m) => LensT b v b m v+lGet = LensT $ do+           !l <- ask+           !b <- get+           return $! l ^$ b+{-# INLINE lGet #-}+++------------------------------------------------------------------------------+lPut :: (Monad m) => v -> LensT b v b m ()+lPut v = LensT $ do+             !l <- ask+             !b <- get+             put $! (l ^!= v) b+{-# INLINE lPut #-}+++------------------------------------------------------------------------------+runLensT :: (Monad m) =>+            LensT b v s m a+         -> Lens b v+         -> s+         -> m (a, s)+runLensT (LensT m) = runRST m+{-# INLINE runLensT #-}+++------------------------------------------------------------------------------+withLensT :: Monad m =>+             ((Lens b' v') -> (Lens b v))+          -> LensT b v s m a+          -> LensT b' v' s m a+withLensT f (LensT m) = LensT $ withRST f m+{-# INLINE withLensT #-}+++------------------------------------------------------------------------------+withTop :: Monad m+        => (Lens b v')+        -> LensT b v' s m a+        -> LensT b v  s m a+withTop !subLens = withLensT (const subLens)+{-# INLINE withTop #-}+++------------------------------------------------------------------------------+with :: Monad m+     => (Lens v v')+     -> LensT b v' s m a+     -> LensT b v  s m a+with !subLens = withLensT (subLens .)+{-# INLINE with #-}++
+ src/Snap/Snaplet/Internal/Lensed.hs view
@@ -0,0 +1,151 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}++module Snap.Snaplet.Internal.Lensed where++import Control.Applicative+import Control.Monad+import Control.Monad.Trans+import Data.Lens.Strict+import Control.Monad.CatchIO+import Control.Monad.Reader.Class+import Control.Monad.State.Class+import Control.Monad.State.Strict+import Control.Category+import Prelude hiding (catch, id, (.))+import Snap.Core+++------------------------------------------------------------------------------+newtype Lensed b v m a = Lensed+    { unlensed :: Lens b v -> v -> b -> m (a, v, b) }+++------------------------------------------------------------------------------+instance Functor m => Functor (Lensed b v m) where+    fmap f (Lensed g) = Lensed $ \l v s ->+        (\(a,v',s') -> (f a, v', s')) <$> g l v s+++------------------------------------------------------------------------------+instance (Functor m, Monad m) => Applicative (Lensed b v m) where+    pure a = Lensed $ \_ v s -> return (a, v, s)+    Lensed mf <*> Lensed ma = Lensed $ \l v s -> do+        (f, v', s') <- mf l v s+        (\(a,v'',s'') -> (f a, v'', s'')) <$> ma l v' s'+++------------------------------------------------------------------------------+instance Monad m => Monad (Lensed b v m) where+    return a = Lensed $ \_ v s -> return (a, v, s)+    Lensed g >>= k = Lensed $ \l v s -> do+        (a, v', s') <- g l v s+        unlensed (k a) l v' s'+++------------------------------------------------------------------------------+instance Monad m => MonadState v (Lensed b v m) where+    get = Lensed $ \_ v s -> return (v, v, s)+    put v' = Lensed $ \_ _ s -> return ((), v', s)+++------------------------------------------------------------------------------+instance Monad m => MonadReader (Lens b v) (Lensed b v m) where+    ask = Lensed $ \l v s -> return (l, v, s)+    local f g = do+        l' <- asks f+        withTop l' g+++------------------------------------------------------------------------------+instance MonadTrans (Lensed b v) where+    lift m = Lensed $ \_ v b -> do+                 res <- m+                 return (res, v, b)+++------------------------------------------------------------------------------+instance MonadIO m => MonadIO (Lensed b v m) where+  liftIO = lift . liftIO+++------------------------------------------------------------------------------+instance MonadCatchIO m => MonadCatchIO (Lensed b v m) where+    catch (Lensed m) f = Lensed $ \l v b -> m l v b `catch` handler l v b+      where+        handler l v b e = let (Lensed h) = f e+                          in h l v b++    block (Lensed m)   = Lensed $ \l v b -> block (m l v b)+    unblock (Lensed m) = Lensed $ \l v b -> unblock (m l v b)+++------------------------------------------------------------------------------+instance MonadPlus m => MonadPlus (Lensed b v m) where+    mzero = lift mzero+    m `mplus` n = Lensed $ \l v b ->+                  unlensed m l v b `mplus` unlensed n l v b+++------------------------------------------------------------------------------+instance (Monad m, Alternative m) => Alternative (Lensed b v m) where+    empty = lift empty+    (Lensed m) <|> (Lensed n) = Lensed $ \l v b -> m l v b <|> n l v b+++------------------------------------------------------------------------------+instance MonadSnap m => MonadSnap (Lensed b v m) where+    liftSnap = lift . liftSnap+++------------------------------------------------------------------------------+getBase :: Monad m => Lensed b v m b+getBase = Lensed $ \_ v b -> return (b, v, b)+++------------------------------------------------------------------------------+withTop :: Monad m => Lens b v' -> Lensed b v' m a -> Lensed b v m a+withTop l m = globally $ lensedAsState m l+++------------------------------------------------------------------------------+with :: Monad m => Lens v v' -> Lensed b v' m a -> Lensed b v m a+with l g = do+    l' <- asks (l .)+    withTop l' g+++------------------------------------------------------------------------------+embed :: Monad m => Lens v v' -> Lensed v v' m a -> Lensed b v m a+embed l m = locally $ lensedAsState m l+++------------------------------------------------------------------------------+globally :: Monad m => StateT b m a -> Lensed b v m a+globally (StateT f) = Lensed $ \l v s ->+                      liftM (\(a, s') -> (a, l ^$ s', s')) $ f (l ^= v $ s)+++------------------------------------------------------------------------------+locally :: Monad m => StateT v m a -> Lensed b v m a+locally (StateT f) = Lensed $ \_ v s ->+                     liftM (\(a, v') -> (a, v', s)) $ f v+++------------------------------------------------------------------------------+lensedAsState :: Monad m => Lensed b v m a -> Lens b v -> StateT b m a+lensedAsState (Lensed f) l = StateT $ \s -> do+    (a, v', s') <- f l (l ^$ s) s+    return (a, l ^= v' $ s')+++------------------------------------------------------------------------------+runLensed :: Monad m+          => Lensed t1 b m t+          -> Lens t1 b+          -> t1+          -> m (t, t1)+runLensed (Lensed f) l s = do+    (a, v', s') <- f l (l ^$ s) s+    return (a, l ^= v' $ s')+
+ src/Snap/Snaplet/Internal/RST.hs view
@@ -0,0 +1,109 @@+{-# LANGUAGE BangPatterns               #-}+{-# LANGUAGE FlexibleInstances          #-}+{-# LANGUAGE MultiParamTypeClasses      #-}++module Snap.Snaplet.Internal.RST where++import           Control.Applicative+import           Control.Category+import           Control.Monad.CatchIO+import           Control.Monad.Reader+import           Control.Monad.State.Class+import           Prelude hiding ((.), id, catch)+import           Snap.Core+++------------------------------------------------------------------------------+-- like RWST, but no writer to bog things down. Also assured strict, inlined+-- monad bind, etc+newtype RST r s m a = RST { runRST :: r -> s -> m (a, s) }+++evalRST :: Monad m => RST r s m a -> r -> s -> m a+evalRST m r s = do+    (a,_) <- runRST m r s+    return a+{-# INLINE evalRST #-}+++execRST :: Monad m => RST r s m a -> r -> s -> m s+execRST m r s = do+    (_,!s') <- runRST m r s+    return s'+{-# INLINE execRST #-}+++withRST :: Monad m => (r' -> r) -> RST r s m a -> RST r' s m a+withRST f m = RST $ \r' s -> runRST m (f r') s+{-# INLINE withRST #-}+++instance (Monad m) => MonadReader r (RST r s m) where+    ask = RST $ \r s -> return (r,s)+    local f m = RST $ \r s -> runRST m (f r) s+++instance (Functor m) => Functor (RST r s m) where+    fmap f m = RST $ \r s -> fmap (\(a,s') -> (f a, s')) $ runRST m r s+++instance (Functor m, Monad m) => Applicative (RST r s m) where+    pure = return+    (<*>) = ap+++instance (Functor m, MonadPlus m) => Alternative (RST r s m) where+    empty = mzero+    (<|>) = mplus+++instance (Monad m) => MonadState s (RST r s m) where+    get   = RST $ \_ s -> return (s,s)+    put x = RST $ \_ _ -> return ((),x)+++mapRST :: (m (a, s) -> n (b, s)) -> RST r s m a -> RST r s n b+mapRST f m = RST $ \r s -> f (runRST m r s)+++instance (MonadCatchIO m) => MonadCatchIO (RST r s m) where+    m `catch` f = RST $ \r s -> runRST m r s+                           `catch` \e -> runRST (f e) r s+    block       = mapRST block+    unblock     = mapRST unblock++instance (MonadSnap m) => MonadSnap (RST r s m) where+    liftSnap s = lift $ liftSnap s++rwsBind :: Monad m =>+           RST r s m a+        -> (a -> RST r s m b)+        -> RST r s m b+rwsBind m f = RST go+  where+    go r !s = do+        (a, !s')  <- runRST m r s+        runRST (f a) r s'+{-# INLINE rwsBind #-}++instance (Monad m) => Monad (RST r s m) where+    return a = RST $ \_ s -> return (a, s)+    (>>=)    = rwsBind+    fail msg = RST $ \_ _ -> fail msg+++instance (MonadPlus m) => MonadPlus (RST r s m) where+    mzero       = RST $ \_ _ -> mzero+    m `mplus` n = RST $ \r s -> runRST m r s `mplus` runRST n r s+++instance MonadTrans (RST r s) where+    lift m = RST $ \_ s -> do+        a <- m+        return $ s `seq` (a, s)++instance (MonadIO m) => MonadIO (RST r s m) where+    liftIO = lift . liftIO+++
+ src/Snap/Snaplet/Internal/Types.hs view
@@ -0,0 +1,340 @@+{-# LANGUAGE BangPatterns               #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses      #-}+{-# LANGUAGE OverloadedStrings          #-}+{-# LANGUAGE TemplateHaskell            #-}+{-# LANGUAGE FlexibleInstances          #-}++module Snap.Snaplet.Internal.Types where++import           Prelude hiding ((.))+import           Control.Applicative+import           Control.Category ((.))+import           Control.Monad.CatchIO hiding (Handler)+import           Control.Monad.Reader+import           Control.Monad.State.Class+import           Control.Monad.Trans.Writer hiding (pass)+import           Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as B+import           Data.Configurator.Types+import           Data.IORef+import           Data.Monoid+import           Data.Lens.Lazy+import           Data.Lens.Template+import           Data.Text (Text)+import qualified Data.Text as T++import           Snap.Core+import qualified Snap.Snaplet.Internal.LensT as LT+import qualified Snap.Snaplet.Internal.Lensed as L+++------------------------------------------------------------------------------+-- | An opaque data type holding internal snaplet configuration data.  It is+-- exported publicly because the getOpaqueConfig function in MonadSnaplet+-- makes implementing new instances of MonadSnaplet more convenient.+data SnapletConfig = SnapletConfig+    { _scAncestry        :: [Text]+    , _scFilePath        :: FilePath+    , _scId              :: Maybe Text+    , _scDescription     :: Text+    , _scUserConfig      :: Config+    , _scRouteContext    :: [ByteString]+    , _reloader          :: IO (Either String String) -- might change+    }+++------------------------------------------------------------------------------+-- | Joins a reversed list of directories into a path.+buildPath :: [ByteString] -> ByteString+buildPath ps = B.intercalate "/" $ reverse ps+++------------------------------------------------------------------------------+-- | Joins a reversed list of directories into a path.+getRootURL :: SnapletConfig -> ByteString+getRootURL sc = buildPath $ _scRouteContext sc+++------------------------------------------------------------------------------+-- | Snaplet's type parameter 's' here is user-defined and can be any Haskell+-- type.  A value of type @Snaplet s@ countains a couple of things:+--+-- * a value of type @s@, called the \"user state\".+--+-- * some bookkeeping data the framework uses to plug things together, like+--   the snaplet's configuration, the snaplet's root directory on the+--   filesystem, the snaplet's root URL, and so on.+data Snaplet s = Snaplet+    { _snapletConfig :: SnapletConfig+    , _snapletValue  :: s+    }+++makeLenses [''SnapletConfig, ''Snaplet]+++------------------------------------------------------------------------------+-- | A lens referencing the opaque SnapletConfig data type held inside+-- Snaplet.+snapletConfig :: Lens (Snaplet a) SnapletConfig+++------------------------------------------------------------------------------+-- | A lens referencing the user-defined state type wrapped by a Snaplet.+snapletValue :: Lens (Snaplet a) a+++------------------------------------------------------------------------------+-- | Transforms a lens of the type you get from makeLenses to an similar lens+-- that is more suitable for internal use.+subSnaplet :: (Lens a (Snaplet b)) -> (Lens (Snaplet a) (Snaplet b))+subSnaplet = (. snapletValue)+++------------------------------------------------------------------------------+-- | The m type parameter used in the MonadSnaplet type signatures will+-- usually be either Initializer or Handler, but other monads may sometimes be+-- useful.+--+-- Minimal complete definition:+--+-- * 'withTop'', 'with'', 'getLens', and 'getOpaqueConfig'.+--+class MonadSnaplet m where+    -- | Runs a child snaplet action in the current snaplet's context.  If you+    -- think about snaplet lenses using a filesystem path metaphor, the lens+    -- supplied to this snaplet must be a relative path.  In other words, the+    -- lens's base state must be the same as the current snaplet.+    with :: (Lens v (Snaplet v'))+         -- ^ A relative lens identifying a snaplet+         -> m b v' a+         -- ^ Action from the lense's snaplet+         -> m b v a+    with = with' . subSnaplet++    -- | Like 'with' but doesn't impose the requirement that the action+    -- being run be a descendant of the current snaplet.  Using our filesystem+    -- metaphor again, the lens for this function must be an absolute+    -- path--it's base must be the same as the current base.+    withTop :: (Lens b (Snaplet v'))+            -- ^ An \"absolute\" lens identifying a snaplet+            -> m b v' a+            -- ^ Action from the lense's snaplet+            -> m b v a+    withTop l = withTop' (subSnaplet l)++    -- | A variant of 'with' accepting a lens from snaplet to snaplet.  Unlike+    -- the lens used in the above 'with' function, this lens formulation has+    -- an identity, which makes it useful in certain circumstances.  The+    -- lenses generated by 'makeLenses' will not work with this function,+    -- however the lens returned by 'getLens' will.+    --+    -- @with = with' . subSnaplet@+    with' :: (Lens (Snaplet v) (Snaplet v')) -> m b v' a -> m b v a++    -- Not providing a definition for this function in terms of withTop'+    -- allows us to avoid extra Monad type class constraints, making the type+    -- signature easier to read.+    -- with' l m = flip withTop m . (l .) =<< getLens++    -- | The absolute version of 'with''+    withTop' :: (Lens (Snaplet b) (Snaplet v')) -> m b v' a -> m b v a++    -- | Gets the lens for the current snaplet.+    getLens :: m b v (Lens (Snaplet b) (Snaplet v))++    -- | Gets the current snaplet's opaque config data type.  You'll only use+    -- this function when writing MonadSnaplet instances.+    getOpaqueConfig :: m b v SnapletConfig+    -- NOTE: We can't just use a MonadState (Snaplet v) instance for this+    -- because Initializer has SnapletConfig, but doesn't have a full Snaplet.+++------------------------------------------------------------------------------+-- | Gets a list of the names of snaplets that are direct ancestors of the+-- current snaplet.+getSnapletAncestry :: (Monad (m b v), MonadSnaplet m) => m b v [Text]+getSnapletAncestry = return . _scAncestry =<< getOpaqueConfig+++------------------------------------------------------------------------------+-- | Gets the snaplet's path on the filesystem.+getSnapletFilePath :: (Monad (m b v), MonadSnaplet m) => m b v FilePath+getSnapletFilePath = return . _scFilePath =<< getOpaqueConfig+++------------------------------------------------------------------------------+-- | Gets the current snaple's name.+getSnapletName :: (Monad (m b v), MonadSnaplet m) => m b v (Maybe Text)+getSnapletName = return . _scId =<< getOpaqueConfig+++------------------------------------------------------------------------------+-- | Gets a human readable description of the snaplet.+getSnapletDescription :: (Monad (m b v), MonadSnaplet m) => m b v Text+getSnapletDescription = return . _scDescription =<< getOpaqueConfig+++------------------------------------------------------------------------------+-- | Gets the config data structure for the current snaplet.+getSnapletUserConfig :: (Monad (m b v), MonadSnaplet m) => m b v Config+getSnapletUserConfig = return . _scUserConfig =<< getOpaqueConfig+++------------------------------------------------------------------------------+-- | Gets the base URL for the current snaplet.  Directories get added to+-- the current snaplet path by calls to 'nestSnaplet'.+getSnapletRootURL :: (Monad (m b v), MonadSnaplet m) => m b v ByteString+getSnapletRootURL = liftM getRootURL getOpaqueConfig+++------------------------------------------------------------------------------+-- | Snaplet infrastructure is available during runtime request processing+-- through the Handler monad.  There aren't very many standalone functions to+-- read about here, but this is deceptive.  The key is in the type class+-- instances.  Handler is an instance of 'MonadSnap', which means it is the+-- monad you will use to write all your application routes.  It also has a+-- 'MonadSnaplet' instance, which gives you all the functionality described+-- above.+newtype Handler b v a =+    Handler (L.Lensed (Snaplet b) (Snaplet v) Snap a)+  deriving ( Monad+           , Functor+           , Applicative+           , MonadIO+           , MonadPlus+           , MonadCatchIO+           , Alternative+           , MonadSnap)+++------------------------------------------------------------------------------+-- | Gets the @Snaplet v@ from the current snaplet's state.+getSnapletState :: Handler b v (Snaplet v)+getSnapletState = Handler get+++------------------------------------------------------------------------------+-- | Puts a new @Snaplet v@ in the current snaplet's state.+putSnapletState :: Snaplet v -> Handler b v ()+putSnapletState = Handler . put+++------------------------------------------------------------------------------+-- | Modifies the @Snaplet v@ in the current snaplet's state.+modifySnapletState :: (Snaplet v -> Snaplet v) -> Handler b v ()+modifySnapletState f = do+    s <- getSnapletState+    putSnapletState (f s)+++------------------------------------------------------------------------------+-- | Gets the @Snaplet v@ from the current snaplet's state and applies a+-- function to it.+getsSnapletState :: (Snaplet v -> b) -> Handler b1 v b+getsSnapletState f = do+    s <- getSnapletState+    return (f s)+++------------------------------------------------------------------------------+-- | The MonadState instance gives you access to the current snaplet's state.+instance MonadState v (Handler b v) where+    get = getsSnapletState _snapletValue+    put v = modifySnapletState (setL snapletValue v)+++instance MonadSnaplet Handler where+    getLens = Handler ask+    with' !l (Handler !m) = Handler $ L.with l m+    withTop' !l (Handler m) = Handler $ L.withTop l m+    getOpaqueConfig = Handler $ gets _snapletConfig+++------------------------------------------------------------------------------+-- | Handler that reloads the site.+reloadSite :: Handler b v ()+reloadSite = failIfNotLocal $ do+    cfg <- getOpaqueConfig+    !res <- liftIO $ _reloader cfg+    either bad good res+  where+    bad msg = do+        writeText $ "Error reloading site!\n\n"+        writeText $ T.pack msg+    good msg = do+        writeText $ T.pack msg+        writeText $ "Site successfully reloaded.\n"+    failIfNotLocal m = do+        rip <- liftM rqRemoteAddr getRequest+        if not $ elem rip [ "127.0.0.1"+                          , "localhost"+                          , "::1" ]+          then pass+          else m+++------------------------------------------------------------------------------+-- | Information about a partially constructed initializer.  Used to+-- automatically aggregate handlers and cleanup actions.+data InitializerState b = InitializerState+    { _isTopLevel      :: Bool+    , _cleanup         :: IO ()+    , _handlers        :: [(ByteString, Handler b b ())]+    -- ^ Handler routes built up and passed to route.+    , _hFilter         :: Handler b b () -> Handler b b ()+    -- ^ Generic filtering of handlers+    , _curConfig       :: SnapletConfig+    -- ^ This snaplet config is the incrementally built config for whatever+    -- snaplet is currently being constructed.+    , _initMessages    :: IORef Text+    }+++------------------------------------------------------------------------------+-- | Wrapper around IO actions that modify state elements created during+-- initialization.+newtype Hook a = Hook (Snaplet a -> IO (Snaplet a))+++instance Monoid (Hook a) where+    mempty = Hook return+    (Hook a) `mappend` (Hook b) = Hook (a >=> b)+++------------------------------------------------------------------------------+-- | Monad used for initializing snaplets.+newtype Initializer b v a =+    Initializer (LT.LensT (Snaplet b)+                          (Snaplet v)+                          (InitializerState b)+                          (WriterT (Hook b) IO)+                          a)+  deriving (Applicative, Functor, Monad, MonadIO)++makeLenses [''InitializerState]+++instance MonadSnaplet Initializer where+    getLens = Initializer ask+    with' !l (Initializer !m) = Initializer $ LT.with l m+    withTop' !l (Initializer m) = Initializer $ LT.withTop l m+    getOpaqueConfig = Initializer $ liftM _curConfig LT.getBase+++------------------------------------------------------------------------------+-- | Opaque newtype which gives us compile-time guarantees that the user is+-- using makeSnaplet and either nestSnaplet or embedSnaplet correctly.+newtype SnapletInit b v = SnapletInit (Initializer b v (Snaplet v))+++------------------------------------------------------------------------------+-- | Information needed to reload a site.  Instead of having snaplets define+-- their own reload actions, we store the original site initializer and use it+-- instead.+data ReloadInfo b = ReloadInfo+    { riRef     :: IORef (Snaplet b)+    , riAction  :: Initializer b b b+    }+
+ src/Snap/Snaplet/Session.hs view
@@ -0,0 +1,107 @@+module Snap.Snaplet.Session++(+    SessionManager+  , withSession+  , commitSession+  , setInSession+  , getFromSession+  , deleteFromSession+  , csrfToken+  , sessionToList+  , resetSession+  , touchSession++) where++import           Control.Monad.State+import           Data.Lens.Lazy+import           Data.Text (Text)++import           Snap.Snaplet+import           Snap.Core++import           Snap.Snaplet.Session.SessionManager+                   ( SessionManager(..), ISessionManager(..) )+import qualified Snap.Snaplet.Session.SessionManager as SM++++-- | Wrap around a handler, committing any changes in the session at the end+withSession :: (Lens b (Snaplet SessionManager))+            -> Handler b v a+            -> Handler b v a+withSession l h = do+  a <- h+  withTop l commitSession+  return a+++-- | Commit changes to session within the current request cycle+commitSession :: Handler b SessionManager ()+commitSession = do+  SessionManager b <- loadSession+  liftSnap $ commit b+++-- | Set a key-value pair in the current session+setInSession :: Text -> Text -> Handler b SessionManager ()+setInSession k v = do+  SessionManager r <- loadSession+  let r' = SM.insert k v r+  put $ SessionManager r'+++-- | Get a key from the current session+getFromSession :: Text -> Handler b SessionManager (Maybe Text)+getFromSession k = do+  SessionManager r <- loadSession+  return $ SM.lookup k r+++-- | Remove a key from the current session+deleteFromSession :: Text -> Handler b SessionManager ()+deleteFromSession k = do+  SessionManager r <- loadSession+  let r' = SM.delete k r+  put $ SessionManager r'+++-- | Returns a CSRF Token unique to the current session+csrfToken :: Handler b SessionManager Text+csrfToken = do+  mgr@(SessionManager r) <- loadSession+  put mgr+  return $ SM.csrf r+++-- | Return session contents as an association list+sessionToList :: Handler b SessionManager [(Text, Text)]+sessionToList = do+  SessionManager r <- loadSession+  return $ SM.toList r+++-- | Deletes the session cookie, effectively resetting the session+resetSession :: Handler b SessionManager ()+resetSession = do+  SessionManager r <- loadSession+  r' <- liftSnap $ SM.reset r+  put $ SessionManager r'+++-- | Touch the session so the timeout gets refreshed+touchSession :: Handler b SessionManager ()+touchSession = do+  SessionManager r <- loadSession+  let r' = SM.touch r+  put $ SessionManager r'+++-- | Load the session into the manager+loadSession :: Handler b SessionManager SessionManager+loadSession = do+  SessionManager r <- get+  r' <- liftSnap $ load r+  return $ SessionManager r'+
+ src/Snap/Snaplet/Session/Backends/CookieSession.hs view
@@ -0,0 +1,160 @@+{-# LANGUAGE OverloadedStrings          #-}+{-# LANGUAGE DeriveDataTypeable         #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++module Snap.Snaplet.Session.Backends.CookieSession++( initCookieSessionManager ) where++import           Control.Monad.Reader+import           Data.ByteString (ByteString)+import           Data.Generics+import           Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as HM+import           Data.Hashable (Hashable)+import           Data.Serialize (Serialize)+import qualified Data.Serialize as S+import           Data.Text (Text)+import           Web.ClientSession++import           Snap.Core (Snap)+import           Snap.Snaplet+import           Snap.Snaplet.Session.Common (mkCSRFToken)+import           Snap.Snaplet.Session.SessionManager+import           Snap.Snaplet.Session.SecureCookie+++-- | Session data are kept in a 'HashMap' for this backend+type Session = HashMap Text Text+++-- | This is what the 'Payload' will be for the CookieSession backend+data CookieSession = CookieSession+  { csCSRFToken :: Text+  , csSession :: Session+} deriving (Eq, Show)+++instance Serialize CookieSession where+  put (CookieSession a b) = S.put (a,b)+  get                     = (\(a,b) -> CookieSession a b) `fmap` S.get++instance (Serialize k, Serialize v, Hashable k, Eq k) =>+  Serialize (HashMap k v) where+  put = S.put . HM.toList+  get = HM.fromList `fmap` S.get+++mkCookieSession :: IO CookieSession+mkCookieSession = do+  t <- liftIO $ mkCSRFToken+  return $ CookieSession t HM.empty+++-- | The manager data type to be stuffed into 'SessionManager'+data CookieSessionManager = CookieSessionManager {+    session :: Maybe CookieSession+  -- ^ Per request cache for 'CookieSession'++  , siteKey :: Key+  -- ^ A long encryption key used for secure cookie transport++  , cookieName :: ByteString+  -- ^ Cookie name for the session system++  , timeOut :: Maybe Int+  -- ^ Session cookies will be considered "stale" after this many seconds.+} deriving (Show,Typeable)+++loadDefSession :: CookieSessionManager -> IO CookieSessionManager+loadDefSession mgr@(CookieSessionManager ses _ _ _) = do+  case ses of+    Nothing -> do+      ses' <- mkCookieSession+      return $ mgr { session = Just ses' }+    Just _ -> return mgr+++modSession :: (Session -> Session) -> CookieSession -> CookieSession+modSession f (CookieSession t ses) = CookieSession t (f ses)+++-- | Initialize a cookie-backed session, returning a 'SessionManager' to be+-- stuffed inside your application's state. This 'SessionManager' will enable+-- the use of all session storage functionality defined in+-- 'Snap.Snaplet.Session'+initCookieSessionManager+  :: FilePath             -- ^ Path to site-wide encryption key+  -> ByteString           -- ^ Session cookie name+  -> Maybe Int            -- ^ Session time-out (replay attack protection)+  -> SnapletInit b SessionManager+initCookieSessionManager fp cn to =+  makeSnaplet "CookieSession" "A snaplet providing sessions via HTTP cookies."+         Nothing $ liftIO $ do+    key <- getKey fp+    return . SessionManager $ CookieSessionManager Nothing key cn to+++instance ISessionManager CookieSessionManager where+  load mgr@(CookieSessionManager r _ _ _) = do+    case r of+      Just _ -> return mgr+      Nothing -> do+        pl <- getPayload mgr+        case pl of+          Nothing -> liftIO $ loadDefSession mgr+          Just (Payload x) -> do+            let c = S.decode x+            case c of+              Left _ -> liftIO $ loadDefSession mgr+              Right cs -> return $ mgr { session = Just cs }++  commit mgr@(CookieSessionManager r _ _ _) = do+    pl <- case r of+      Just r' -> return . Payload $ S.encode r'+      Nothing -> liftIO mkCookieSession >>= return . Payload . S.encode+    setPayload mgr pl++  reset mgr = do+    cs <- liftIO mkCookieSession+    return $ mgr { session = Just cs }++  touch = id++  insert k v mgr@(CookieSessionManager r _ _ _) = case r of+    Just r' -> mgr { session = Just $ modSession (HM.insert k v) r' }+    Nothing -> mgr++  lookup k (CookieSessionManager r _ _ _) = r >>= HM.lookup k . csSession++  delete k mgr@(CookieSessionManager r _ _ _) = case r of+    Just r' -> mgr { session = Just $ modSession (HM.delete k) r' }+    Nothing -> mgr++  csrf (CookieSessionManager r _ _ _) = case r of+    Just r' -> csCSRFToken r'+    Nothing -> ""++  toList (CookieSessionManager r _ _ _) = case r of+    Just r' -> HM.toList . csSession $ r'+    Nothing -> []++++-- | A session payload to be stored in a SecureCookie.+newtype Payload = Payload ByteString+  deriving (Eq, Show, Ord, Serialize)+++-- | Get the current client-side value+getPayload :: CookieSessionManager -> Snap (Maybe Payload)+getPayload mgr = getSecureCookie (cookieName mgr) (siteKey mgr) (timeOut mgr)+++-- | Set the client-side value+setPayload :: CookieSessionManager -> Payload -> Snap ()+setPayload mgr x =+    setSecureCookie (cookieName mgr) (siteKey mgr) (timeOut mgr) x++
+ src/Snap/Snaplet/Session/Common.hs view
@@ -0,0 +1,41 @@+{-|++  This module contains functionality common among multiple back-ends.++-}++module Snap.Snaplet.Session.Common where+++import           Numeric+import           Data.Serialize+import qualified Data.Serialize as S+import           Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as B+import qualified Data.Text.Encoding as T+import           Data.Text (Text)+import           System.Random.MWC+++------------------------------------------------------------------------------+-- | Generates a random salt of given length+randomToken :: Int -> IO ByteString+randomToken n =+  let+    mk :: GenIO -> IO Int+    mk gen = uniformR (0,15) gen+  in do+    is <- withSystemRandom $ \gen -> sequence . take n . repeat $ mk gen+    return . B.pack . concat . map (flip showHex "") $ is+++------------------------------------------------------------------------------+-- | Generate a randomized CSRF token+mkCSRFToken :: IO Text+mkCSRFToken = T.decodeUtf8 `fmap` randomToken 40+++instance Serialize Text where+  put = S.put . T.encodeUtf8+  get = T.decodeUtf8 `fmap` S.get+
+ src/Snap/Snaplet/Session/SecureCookie.hs view
@@ -0,0 +1,96 @@+{-# LANGUAGE OverloadedStrings #-}+{-|++  This is a support module meant to back all session back-end implementations.++  It gives us an encrypted and timestamped cookie that can store an arbitrary+  serializable payload. For security, it will:++    * Encrypt its payload together with a timestamp.++    * Check the timestamp for session expiration everytime you read from the+    cookie. This will limit intercept-and-replay attacks by disallowing+    cookies older than the timeout threshold.++-}++module Snap.Snaplet.Session.SecureCookie where++import Control.Applicative+import Control.Monad+import Control.Monad.Trans++import Data.ByteString (ByteString)+import Data.Time+import Data.Time.Clock.POSIX++import Data.Serialize+import Web.ClientSession++import Snap.Core++++------------------------------------------------------------------------------+-- | Serialize UTCTime+instance Serialize UTCTime where+    put t = put (round (utcTimeToPOSIXSeconds t) :: Integer)+    get   = posixSecondsToUTCTime . fromInteger <$> get+++------------------------------------------------------------------------------+-- | Arbitrary payload with timestamp.+type SecureCookie t = (UTCTime, t)+++------------------------------------------------------------------------------+-- Get the payload back+getSecureCookie :: (MonadSnap m, Serialize t)+                => ByteString       -- ^ Cookie name+                -> Key              -- ^ Encryption key+                -> Maybe Int        -- ^ Timeout in seconds+                -> m (Maybe t)+getSecureCookie name key timeout = do+    rqCookie <- getCookie name+    rspCookie <- getResponseCookie name `fmap` getResponse+    let ck = rspCookie `mplus` rqCookie+    let val = fmap cookieValue ck >>= decrypt key >>= return . decode+    let val' = val >>= either (const Nothing) Just+    case val' of+      Nothing -> return Nothing+      Just (ts, t) -> do+        to <- checkTimeout timeout ts+        return $ case to of+          True -> Nothing+          False -> Just t+++------------------------------------------------------------------------------+-- | Inject the payload+setSecureCookie :: (MonadSnap m, Serialize t)+                => ByteString       -- ^ Cookie name+                -> Key              -- ^ Encryption key+                -> Maybe Int        -- ^ Max age in seconds+                -> t                -- ^ Serializable payload+                -> m ()+setSecureCookie name key to val = do+    t <- liftIO getCurrentTime+    let expire = to >>= Just . flip addUTCTime t . fromIntegral+    val' <- liftIO . encryptIO key . encode $ (t, val)+    let nc = Cookie name val' expire Nothing (Just "/") False True+    modifyResponse $ addResponseCookie nc+++------------------------------------------------------------------------------+-- | Validate session against timeout policy.+--+-- * If timeout is set to 'Nothing', never trigger a time-out.+-- * Othwerwise, do a regular time-out check based on current time and given+-- timestamp.+checkTimeout :: (MonadSnap m) => Maybe Int -> UTCTime -> m Bool+checkTimeout Nothing _ = return False+checkTimeout (Just x) t0 =+  let x' = fromIntegral x+  in do+      t1 <- liftIO getCurrentTime+      return $ t1 > addUTCTime x' t0
+ src/Snap/Snaplet/Session/SessionManager.hs view
@@ -0,0 +1,49 @@+{-# LANGUAGE ExistentialQuantification #-}++module Snap.Snaplet.Session.SessionManager where++import           Data.Text (Text)+import           Prelude hiding (lookup)++import           Snap.Core (Snap)+++-- | Any Haskell record that is a member of the 'ISessionManager' typeclass+-- can be stuffed inside a 'SessionManager' to enable all session-related+-- functionality.+data SessionManager = forall a. ISessionManager a => SessionManager a+++class ISessionManager r where++  -- | Load a session from given payload.+  --+  -- Will always be called before any other operation. If possible, cache and+  -- do nothing when called multiple times within the same request cycle.+  load :: r -> Snap r++  -- | Commit session, return a possibly updated paylaod+  commit :: r -> Snap ()++  -- | Reset session+  reset :: r -> Snap r++  -- | Touch session+  touch :: r -> r++  -- | Insert a key-value pair into session+  insert :: Text -> Text -> r -> r++  -- | Lookup a key in session+  lookup :: Text -> r -> (Maybe Text)++  -- | Delete a key in session+  delete :: Text -> r -> r++  -- | Return a session-specific CSRF protection token. See 'mkCSRFToken' for+  -- help in creating the value.+  csrf :: r -> Text++  -- | Return all key-value pairs as an association list+  toList :: r -> [(Text,Text)]+
src/Snap/Starter.hs view
@@ -19,8 +19,9 @@  ------------------------------------------------------------------------------ -- Creates a value tDir :: ([String], [(String, String)])-$(buildData "tDirBareBones" "barebones")-$(buildData "tDirDefault"   "default")+buildData "tDirBareBones" "barebones"+buildData "tDirDefault" "default"+buildData "tDirTutorial" "tutorial"  ------------------------------------------------------------------------------ usage :: String@@ -41,9 +42,30 @@   -------------------------------------------------------------------------------data InitFlag = InitBareBones-              | InitHelp-              | InitExtensions+initUsage :: String+initUsage = unlines+    [ "Snap " ++ (S.unpack snapServerVersion) ++ " Project Kickstarter"+    , ""+    , "Usage:"+    , ""+    , "  snap init [type]"+    , ""+    , "    [type] can be one of:"+    , "      default   - A default project using snaplets and heist"+    , "      barebones - A barebones project with minimal dependencies"+    , "      tutorial  - The literate Haskell tutorial project"+    , ""+    , "  If [type] is omitted, the default project is generated."+    ]+++printUsage :: [String] -> IO ()+printUsage ("init":_) = putStrLn initUsage+printUsage _ = putStrLn usage++------------------------------------------------------------------------------+-- Only one option for now+data Option = Help   deriving (Show, Eq)  @@ -65,42 +87,43 @@ initProject :: [String] -> IO () initProject args = do     case getOpt Permute options args of-      (flags, _, [])-        | InitHelp `elem` flags -> do putStrLn initUsage-                                      exitFailure-        | otherwise             -> init' flags+      (flags, other, [])+        | Help `elem` flags -> do printUsage other+                                  exitFailure+        | otherwise         -> go other+      (_, other, errs) -> do putStrLn $ concat errs+                             printUsage other+                             exitFailure -      (_, _, errs) -> do putStrLn $ concat errs-                         putStrLn initUsage-                         exitFailure   where-    initUsage = usageInfo "Usage\n  snap init\n\nOptions:" options-     options =-        [ Option ['b'] ["barebones"]  (NoArg InitBareBones)-                 "Depend only on -core and -server"-        , Option ['h'] ["help"]       (NoArg InitHelp)+        [ Option ['h'] ["help"]       (NoArg Help)                  "Print this message"-        , Option ['e'] ["extensions"] (NoArg InitExtensions)-                 "Depend on snap w/ extensions (default)"         ] -    init' flags = do+    go ("init":rest) = init' rest+    go _ = do+        putStrLn "Error: Invalid action!"+        putStrLn usage+        exitFailure++    init' args' = do         cur <- getCurrentDirectory         let dirs = splitDirectories cur             projName = last dirs             setup' = setup projName-        case flags of-          (_:_) | InitBareBones  `elem` flags -> setup' tDirBareBones-                | InitExtensions `elem` flags -> setup' tDirDefault-          _                                   -> setup' tDirDefault+        case args' of+          []            -> setup' tDirDefault+          ["barebones"] -> setup' tDirBareBones+          ["default"]   -> setup' tDirDefault+          ["tutorial"]  -> setup' tDirTutorial+          _             -> do+            putStrLn initUsage+            exitFailure   ------------------------------------------------------------------------------ main :: IO () main = do     args <- getArgs-    case args of-        ("init":args') -> initProject args'-        _              -> do putStrLn usage-                             exitFailure+    initProject args
src/Snap/StarterTH.hs view
@@ -7,6 +7,7 @@ import           Language.Haskell.TH import           Language.Haskell.TH.Syntax import           System.Directory.Tree+import           System.FilePath ------------------------------------------------------------------------------  @@ -30,19 +31,18 @@ -- encountered and a list of filenames and content strings. readTree :: FilePath -> IO ([DirData], [FileData]) readTree dir = do-    d <- readDirectory $ dir ++ "/."+    d <- readDirectory $ dir </> "."     let ps = zipPaths $ "" :/ (free d)         fd = F.foldr (:) [] ps-        dirs = tail . getDirs [] $ free d--    return (dirs, fd)+        dirs = getDirs [] $ free d+    return (drop 1 dirs, fd)   ------------------------------------------------------------------------------ -- Calls readTree and returns it's value in a quasiquote. dirQ :: FilePath -> Q Exp dirQ tplDir = do-    d <- runIO . readTree $ "project_template/" ++ tplDir+    d <- runIO . readTree $ "project_template" </> tplDir     lift d  
− test/runTests.sh
@@ -1,21 +0,0 @@-#!/bin/sh--set -e--if [ -z "$DEBUG" ]; then-    export DEBUG=testsuite-fi--SUITE=./dist/build/testsuite/testsuite--if [ ! -f $SUITE ]; then-    cat <<EOF-Testsuite executable not found, please run:-    cabal configure -ftest-then-    cabal build-EOF-    exit;-fi--./dist/build/testsuite/testsuite -j1 $*
+ test/runTestsAndCoverage.sh view
@@ -0,0 +1,62 @@+#!/bin/sh++set -e++if [ -z "$DEBUG" ]; then+    export DEBUG=snap-testsuite+fi++SUITE=./dist/build/snap-testsuite/snap-testsuite++rm -f snap-testsuite.tix++if [ ! -f $SUITE ]; then+    cat <<EOF+Testsuite executable not found, please run:+    cabal configure+then+    cabal build+EOF+    exit;+fi++$SUITE $*++killall -HUP snap-testsuite++DIR=dist/hpc++rm -Rf $DIR+mkdir -p $DIR++EXCLUDES='Main+Blackbox.App+Blackbox.BarSnaplet+Blackbox.Common+Blackbox.EmbeddedSnaplet+Blackbox.FooSnaplet+Blackbox.Tests+Blackbox.Types+Snap.Snaplet.Internal.Lensed.Tests+Snap.Snaplet.Internal.LensT.Tests+Snap.Snaplet.Internal.RST.Tests+Snap.Snaplet.Internal.Tests+Snap.TestCommon+'++EXCL=""++for m in $EXCLUDES; do+    EXCL="$EXCL --exclude=$m"+done++rm -f non-cabal-appdir/templates/bad.tpl+rm -f non-cabal-appdir/templates/good.tpl+rm -fr non-cabal-appdir/snaplets/foosnaplet++hpc markup $EXCL --destdir=$DIR snap-testsuite >/dev/null 2>&1++cat <<EOF++Test coverage report written to $DIR.+EOF
test/snap-testsuite.cabal view
@@ -3,23 +3,107 @@ build-type:     Simple cabal-version:  >= 1.6 -Executable testsuite-  hs-source-dirs:  suite+Executable snap-testsuite+  hs-source-dirs:  ../src suite   main-is:         TestSuite.hs    build-depends:-    QuickCheck >= 2.3.0.2,-    base >= 4 && < 5,-    bytestring == 0.9.*,+    Glob                       >= 0.5 && < 0.7,+    HUnit                      >= 1.2 && < 2,+    MonadCatchIO-transformers  >= 0.2 && < 0.3,+    QuickCheck                 >= 2.3.0.2,+    attoparsec                 (>= 0.8   && < 0.10),+    base                       >= 4 && < 5,+    bytestring                 >= 0.9 && < 0.10,+    containers                 >= 0.3,+    data-lens                  >= 2.0.1 && < 2.1,+    data-lens-template         >= 2.1.1 && < 2.2,     directory,+    directory-tree             >= 0.10 && < 0.11,     filepath,-    Glob == 0.5.*,-    HUnit >= 1.2 && < 2,-    http-enumerator >= 0.7 && <0.8,-    process == 1.*,-    test-framework >= 0.3.1 && <0.5,-    test-framework-hunit >= 0.2.5 && < 0.3,-    test-framework-quickcheck2 >= 0.2.6 && < 0.3-    -  ghc-options: -Wall -fwarn-tabs -funbox-strict-fields -threaded+    heist                      >= 0.6 && < 0.7,+    http-enumerator            >= 0.6.5.3 && < 0.8,+    mtl                        >= 2,+    process                    == 1.*,+    snap-core                  >= 0.6 && < 0.7,+    snap-server                >= 0.6 && < 0.7,+    test-framework             >= 0.4 && < 0.5,+    test-framework-hunit       >= 0.2.5 && < 0.3,+    test-framework-quickcheck2 >= 0.2.6 && < 0.3,+    text                       >= 0.11 && < 0.12,+    transformers               >= 0.2,+    unix                       >= 2.2.0.0 && < 2.6,+    utf8-string                >= 0.3   && < 0.4,+    template-haskell+-- FIXME++  ghc-options: -O2 -Wall -fhpc -fwarn-tabs -funbox-strict-fields -threaded                -fno-warn-unused-do-bind+++Executable app+  hs-source-dirs:  ../src suite+  main-is:         AppMain.hs++  build-depends:+    MonadCatchIO-transformers  >= 0.2 && < 0.3,+    attoparsec                 (>= 0.8   && < 0.10),+    base                       >= 4 && < 5,+    bytestring                 >= 0.9 && < 0.10,+    cereal                     >= 0.3,+    clientsession              >= 0.6.0,+    configurator               == 0.1.*,+    containers                 >= 0.3,+    data-lens                  >= 2.0.1 && < 2.1,+    data-lens-template         >= 2.1.1 && < 2.2,+    directory,+    directory-tree             >= 0.10 && < 0.11,+    filepath,+    hashable                   >= 1.1,+    heist                      >= 0.6 && < 0.7,+    mtl                        >= 2,+    mwc-random                 >= 0.8,+    process                    == 1.*,+    snap-core                  >= 0.6 && < 0.7,+    snap-server                >= 0.6 && < 0.7,+    syb                        >= 0.1,+    time                       >= 1.1,+    text                       >= 0.11 && < 0.12,+    transformers               >= 0.2,+    unordered-containers       >= 0.1.4,+    utf8-string                >= 0.3   && < 0.4,+    template-haskell+    --FIXME++  ghc-options: -O2 -Wall -fwarn-tabs -funbox-strict-fields -threaded+               -fno-warn-unused-do-bind++Executable nesttest+  hs-source-dirs:  ../src suite+  main-is:         NestTest.hs++  build-depends:+    MonadCatchIO-transformers  >= 0.2 && < 0.3,+    attoparsec                 (>= 0.8   && < 0.10),+    base                       >= 4 && < 5,+    bytestring                 >= 0.9 && < 0.10,+    containers                 >= 0.3,+    data-lens                  >= 2.0.1 && < 2.1,+    data-lens-template         >= 2.1 && < 2.2,+    directory,+    directory-tree             >= 0.10 && < 0.11,+    filepath,+    heist                      >= 0.6 && < 0.7,+    mtl                        >= 2,+    process                    == 1.*,+    snap-core                  >= 0.6 && < 0.7,+    snap-server                >= 0.6 && < 0.7,+    text                       >= 0.11 && < 0.12,+    transformers               >= 0.2,+    utf8-string                >= 0.3   && < 0.4,+    template-haskell+    --FIXME++  ghc-options: -O2 -Wall -fwarn-tabs -funbox-strict-fields -threaded+               -fno-warn-unused-do-bind+
test/suite/Snap/TestCommon.hs view
@@ -16,7 +16,9 @@ import System.FilePath.Glob import System.Process hiding (cwd) +import SafeCWD + testGeneratedProject :: String  -- ^ project name and directory                      -> String  -- ^ arguments to @snap init@                      -> String  -- ^ arguments to @cabal install@@@ -24,23 +26,28 @@                      -> IO ()   -- ^ action to run when the server goes up                      -> IO () testGeneratedProject projName snapInitArgs cabalInstallArgs httpPort-                     testAction = bracket initialize cleanup (const testAction)-  where-    initialize = do-        cwd <- getCurrentDirectory-        let projectPath = cwd </> projName-        flip onException (setCurrentDirectory cwd >>-                          removeDirectoryRecursive projectPath) $ do-            makeWorkDirectory projectPath-            setCurrentDirectory projectPath+                     testAction = do+    cwd <- getCurrentDirectory+    let segments = reverse $ splitPath cwd+        projectPath = cwd </> "test-snap-exe" </> projName+        snapRoot = joinPath $ reverse $ drop 1 segments+        snapRepos = joinPath $ reverse $ drop 2 segments++        sandbox = cwd </> "test-cabal-dev"++        cabalDevArgs = "-s " ++ sandbox++        args = cabalDevArgs ++ " " ++ cabalInstallArgs ++        initialize = do             snapExe <- findSnap             systemOrDie $ snapExe ++ " init " ++ snapInitArgs -            snapCoreSrc   <- fromEnv "SNAP_CORE_SRC" "../../../snap-core"-            snapServerSrc <- fromEnv "SNAP_SERVER_SRC" "../../../snap-server"-            xmlhtmlSrc    <- fromEnv "XMLHTML_SRC" "../../../xmlhtml"-            heistSrc      <- fromEnv "HEIST_SRC" "../../../heist"-            let snapSrc   =  "../.."+            snapCoreSrc   <- fromEnv "SNAP_CORE_SRC" $ snapRepos </> "snap-core"+            snapServerSrc <- fromEnv "SNAP_SERVER_SRC" $ snapRepos </> "snap-server"+            xmlhtmlSrc    <- fromEnv "XMLHTML_SRC" $ snapRepos </> "xmlhtml"+            heistSrc      <- fromEnv "HEIST_SRC" $ snapRepos </> "heist"+            let snapSrc   =  snapRoot              forM_ [ "snap-core", "snap-server", "xmlhtml", "heist", "snap" ]                   (pkgCleanUp sandbox)@@ -56,24 +63,32 @@             putStrLn $ "Running \"" ++ cmd ++ "\""             pHandle <- runCommand cmd             waitABit-            return (cwd, projectPath, pHandle)+            return pHandle +        findSnap = do+            home <- fromEnv "HOME" "."+            p1 <- gimmeIfExists $ snapRoot </> "dist" </> "build" </> "snap" </> "snap"+            p2 <- gimmeIfExists $ home </> ".cabal" </> "bin" </> "snap"+            p3 <- findExecutable "snap"++            return $ fromMaybe (error "couldn't find snap executable")+                               (getFirst $ mconcat $ map First [p1,p2,p3])++    putStrLn $ "Changing directory to "++projectPath+    inDir True projectPath $ bracket initialize cleanup (const testAction)+    removeDirectoryRecursiveSafe projectPath+  where+     fromEnv name def = do         r <- getEnv name `catch` \(_::SomeException) -> return ""         if r == "" then return def else return r -    cleanup (cwd, projectPath, pHandle) = do-        setCurrentDirectory cwd+    cleanup pHandle = do         terminateProcess pHandle         waitForProcess pHandle-        removeDirectoryRecursive projectPath      waitABit = threadDelay $ 2*10^(6::Int) -    sandbox = "../test-cabal-dev" </> projName--    cabalDevArgs = "-s " ++ sandbox-     pkgCleanUp d pkg = do         paths <- globDir1 (compile $ "packages*conf/" ++ pkg ++ "-*") d         forM_ paths@@ -83,23 +98,11 @@             putStrLn $ "removing " ++ x             removeFile x -    args = cabalDevArgs ++ " " ++ cabalInstallArgs -     gimmeIfExists p = do         b <- doesFileExist p         if b then return (Just p) else return Nothing  -    findSnap = do-        home <- fromEnv "HOME" "."-        p1 <- gimmeIfExists "../../dist/build/snap/snap"-        p2 <- gimmeIfExists $ home </> ".cabal/bin/snap"-        p3 <- findExecutable "snap"--        return $ fromMaybe (error "couldn't find snap executable")-                           (getFirst $ mconcat $ map First [p1,p2,p3])-              - systemOrDie :: String -> IO () systemOrDie s = do     putStrLn $ "Running \"" ++ s ++ "\""@@ -107,9 +110,3 @@   where     check ExitSuccess = return ()     check _ = throwIO $ ErrorCall $ "command failed: '" ++ s ++ "'"---makeWorkDirectory :: FilePath -> IO ()-makeWorkDirectory p = do-    doesDirectoryExist p >>= flip when (removeDirectoryRecursive p)-    createDirectory p
test/suite/TestSuite.hs view
@@ -2,34 +2,83 @@  module Main where +import           Control.Concurrent+import           Control.Exception import           Control.Monad import qualified Data.ByteString.Lazy.Char8 as L import qualified Data.ByteString.Char8 as S import qualified Network.HTTP.Enumerator as HTTP-import           Test.Framework (defaultMain, Test)+import           Snap.Http.Server.Config+import           Snap.Snaplet+import           System.Posix.Process+import           System.Posix.Types+--import           Test.Framework (defaultMain, Test)+import           Test.Framework import           Test.Framework.Providers.HUnit import           Test.HUnit hiding (Test, path) +import           Snap.Http.Server (simpleHttpServe)+import           Blackbox.App+import qualified Blackbox.Tests+import qualified Snap.Snaplet.Internal.Lensed.Tests+import qualified Snap.Snaplet.Internal.LensT.Tests+import qualified Snap.Snaplet.Internal.RST.Tests+import qualified Snap.Snaplet.Internal.Tests import           Snap.TestCommon +import SafeCWD+++------------------------------------------------------------------------------ main :: IO ()-main = defaultMain tests-  where tests = [ testBarebones+main = do+    Blackbox.Tests.remove "non-cabal-appdir/templates/bad.tpl"+    Blackbox.Tests.remove "non-cabal-appdir/templates/good.tpl"+    Blackbox.Tests.removeDir "non-cabal-appdir/snaplets/foosnaplet"++    inDir False "non-cabal-appdir" startServer+    threadDelay $ 2*10^(6::Int)+    defaultMain tests++  where tests = [ internalServerTests                 , testDefault+                , testBarebones+                , testTutorial                 ]  +internalServerTests :: Test+internalServerTests =+    testGroup "internal server tests"+        [ Blackbox.Tests.tests+        , Snap.Snaplet.Internal.Lensed.Tests.tests+        , Snap.Snaplet.Internal.LensT.Tests.tests+        , Snap.Snaplet.Internal.RST.Tests.tests+        , Snap.Snaplet.Internal.Tests.tests+        ]++startServer :: IO ProcessID+startServer = forkProcess $ serve (setPort 9753 defaultConfig) app+  where+    serve config initializer = do+        (_, handler, doCleanup) <- runSnaplet initializer+        (conf, site)            <- combineConfig config handler+        _ <- try $ simpleHttpServe conf $ site+             :: IO (Either SomeException ())+        doCleanup++ testBarebones :: Test testBarebones = testCase "snap/barebones" go   where     go = testGeneratedProject "barebonesTest"-                              "-b"+                              "barebones"                               ""                               port                               testIt     port = 9990     testIt = do-        body <- HTTP.simpleHttp "http://127.0.0.1:9990"+        body <- HTTP.simpleHttp $ "http://127.0.0.1:"++(show port)         assertEqual "server not up" "hello world" body  @@ -44,9 +93,22 @@     port = 9991     testIt = do         body <- liftM (S.concat . L.toChunks) $-                HTTP.simpleHttp "http://127.0.0.1:9991"+                HTTP.simpleHttp $ "http://127.0.0.1:"++(show port)         assertBool "response contains phrase 'it works!'"                    $ "It works!" `S.isInfixOf` body  --- TODO: test hint code here+testTutorial :: Test+testTutorial = testCase "snap/tutorial" go+  where+    go = testGeneratedProject "tutorialTest"+                              "tutorial"+                              ""+                              port+                              testIt+    port = 9992+    testIt = do+        body <- HTTP.simpleHttp $ "http://127.0.0.1:"++(show port)++"/hello"+        assertEqual "server not up" "hello world" body++