packages feed

snap (empty) → 0.3.0

raw patch · 28 files changed

+2143/−0 lines, 28 filesdep +MonadCatchIO-transformersdep +attoparsecdep +basesetup-changed

Dependencies added: MonadCatchIO-transformers, attoparsec, base, bytestring, bytestring-nums, cereal, containers, directory, directory-tree, dlist, enumerator, filepath, haskell98, heist, hint, mtl, old-locale, old-time, snap-core, snap-server, template-haskell, text, time, unix, unix-compat, zlib

Files

+ CONTRIBUTORS view
@@ -0,0 +1,8 @@+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>
+ LICENSE view
@@ -0,0 +1,27 @@+Copyright (c) 2009, Snap Framework authors (see CONTRIBUTORS)+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++Redistributions of source code must retain the above copyright notice, this+list of conditions and the following disclaimer.++Redistributions in binary form must reproduce the above copyright notice, this+list of conditions and the following disclaimer in the documentation and/or+other materials provided with the distribution.++Neither the name of the Snap Framework authors nor the names of its+contributors may be used to endorse or promote products derived from this+software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.SNAP.md view
@@ -0,0 +1,42 @@+Snap Framework+--------------++Snap is a simple and fast web development framework and server written in+Haskell. For more information or to download the latest version, you can visit+the Snap project website at http://snapframework.com/.+++Snap Status and Features+------------------------++This developer prerelease contains only the Snap core system, namely:++  * a high-speed HTTP server, with an optional high-concurrency backend using+    the [libev](http://software.schmorp.de/pkg/libev.html) library++  * 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++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.+++Snap Philosophy+---------------++Snap aims to be the *de facto* web toolkit for Haskell, on the basis of:++  * High performance++  * High design standards++  * Simplicity and ease of use, even for Haskell beginners++  * Excellent documentation++  * Robustness and high test coverage
+ README.md view
@@ -0,0 +1,62 @@+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)).+++## Library contents++This is the `snap` executable and supporting library, which contains:++  * a command-line utility for creating initial Snap applications++  * a library allowing Snap applications to recompile actions on the+    fly in development mode, with no performance loss in production+    mode.+++Building snap+=============++The snap tool and library are built using+ [Cabal](http://www.haskell.org/cabal/) and+[Hackage](http://hackage.haskell.org/packages/hackage.html). Just run++    cabal install++from the `snap` toplevel directory.+++## Building the Haddock Documentation++The haddock documentation can be built using the supplied `haddock.sh` shell+script:++    ./haddock.sh++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+    $ cabal build++From here you can invoke the testsuite by running:++    $ ./runTestsAndCoverage.sh+++The testsuite generates an `hpc` test coverage report in `test/dist/hpc`.
+ Setup.hs view
@@ -0,0 +1,3 @@+import Distribution.Simple++main = defaultMain
+ project_template/barebones/foo.cabal view
@@ -0,0 +1,29 @@+Name:                projname+Version:             0.1+Synopsis:            Project Synopsis Here+Description:         Project Description Here+License:             AllRightsReserved+Author:              Author+Maintainer:          maintainer@example.com+Stability:           Experimental+Category:            Web+Build-type:          Simple+Cabal-version:       >=1.2++Executable projname+  hs-source-dirs: src+  main-is: Main.hs++  Build-depends:+    base >= 4 && < 5,+    bytestring >= 0.9.1 && < 0.10,+    MonadCatchIO-transformers >= 0.2.1 && < 0.3,+    mtl >= 2 && < 3,+    snap-core >= 0.3 && <0.4,+    snap-server >= 0.3 && <0.4++  if impl(ghc >= 6.12.0)+    ghc-options: -threaded -Wall -fwarn-tabs -funbox-strict-fields -O2+                 -fno-warn-unused-do-bind+  else+    ghc-options: -threaded -Wall -fwarn-tabs -funbox-strict-fields -O2
+ project_template/barebones/src/Main.hs view
@@ -0,0 +1,21 @@+{-# LANGUAGE OverloadedStrings #-}+module Main where++import           Control.Applicative+import           Snap.Types+import           Snap.Util.FileServe+import           Snap.Http.Server++main :: IO ()+main = quickHttpServe $+    ifTop (writeBS "hello world") <|>+    route [ ("foo", writeBS "bar")+          , ("echo/:echoparam", echoHandler)+          ] <|>+    dir "static" (fileServe ".")++echoHandler :: Snap ()+echoHandler = do+    param <- getParam "echoparam"+    maybe (writeBS "must specify echo/param in URL")+          writeBS param
+ project_template/default/foo.cabal view
@@ -0,0 +1,47 @@+Name:                projname+Version:             0.1+Synopsis:            Project Synopsis Here+Description:         Project Description Here+License:             AllRightsReserved+Author:              Author+Maintainer:          maintainer@example.com+Stability:           Experimental+Category:            Web+Build-type:          Simple+Cabal-version:       >=1.2++Flag development+  Description: Whether to build the server in development (interpreted) mode+  Default: False++Executable projname+  hs-source-dirs: src+  main-is: Main.hs++  if !flag(development)+    cpp-options: -DPRODUCTION+  else+    build-depends: hint >= 0.3.2 && < 0.4++  Build-depends:+    base >= 4 && < 5,+    bytestring >= 0.9.1 && < 0.10,+    heist >= 0.4 && < 0.5,+    hexpat >= 0.19 && < 0.20,+    MonadCatchIO-transformers >= 0.2.1 && < 0.3,+    mtl >= 2 && < 3,+    snap >= 0.3 && < 0.4,+    snap-core >= 0.3 && < 0.4,+    snap-server >= 0.3 && <0.4,+    text >= 0.11 && < 0.12,+    time >= 1.1 && < 1.3++  extensions: TypeSynonymInstances MultiParamTypeClasses++  if impl(ghc >= 6.12.0)+    ghc-options: -threaded -Wall -fwarn-tabs -funbox-strict-fields -O2+                 -fno-warn-orphans -fno-warn-unused-do-bind+  else+    ghc-options: -threaded -Wall -fwarn-tabs -funbox-strict-fields -O2+                 -fno-warn-orphans+
+ project_template/default/log/access.log view
+ project_template/default/log/error.log view
+ project_template/default/resources/static/screen.css view
@@ -0,0 +1,26 @@+html {+   padding: 0;+   margin: 0;+   background-color: #ffffff;+   font-family: Verdana, Helvetica, sans-serif;+}+body {+   padding: 0;+   margin: 0;+}+a {+   text-decoration: underline;+}+a :hover {+   cursor: pointer;+   text-decoration: underline;+}+img {+   border: none;+}+#content {+   padding-left: 1em;+}+#info {+   font-size: 60%;+}
+ project_template/default/resources/templates/echo.tpl view
@@ -0,0 +1,14 @@+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">+<html xmlns="http://www.w3.org/1999/xhtml">+  <head>+    <title>Echo Page</title>+  </head>+  <body>+    <div id="content">+      <h1>Is there an echo in here?</h1>+    </div>+    <p>You wanted me to say this?</p>+    <p>"<message/>"</p>+    <p><a href="/">Return</a></p>+  </body>+</html>
+ project_template/default/resources/templates/index.tpl view
@@ -0,0 +1,33 @@+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">+<html xmlns="http://www.w3.org/1999/xhtml">+  <head>+    <title>Snap web server</title>+    <link rel="stylesheet" type="text/css" href="screen.css"/>+  </head>+  <body>+    <div id="content">+      <h1>It works!</h1>+      <p>+        This is a simple demo page served using+        <a href="http://snapframework.com/docs/tutorials/heist">Heist</a>+        and the <a href="http://snapframework.com/">Snap</a> web framework.+      </p>+      <p>+        Echo test:+        <a href="/echo/cats">cats</a>+        <a href="/echo/dogs">dogs</a>+        <a href="/echo/fish">fish</a>+      </p>+      <table id="info">+        <tr>+          <td>Config generated at:</td>+          <td><start-time/></td>+        </tr>+        <tr>+          <td>Page generated at:</td>+          <td><current-time/></td>+        </tr>+      </table>+    </div>+  </body>+</html>
+ project_template/default/src/Application.hs view
@@ -0,0 +1,58 @@+{-++This module defines our application's monad and any application-specific+information it requires.++-}++module Application+  ( Application+  , applicationInitializer+  ) where++import           Snap.Extension+import           Snap.Extension.Heist.Impl+import           Snap.Extension.Timer.Impl+++------------------------------------------------------------------------------+-- | '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+++------------------------------------------------------------------------------+-- | '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+    }+++------------------------------------------------------------------------------+instance HasHeistState Application ApplicationState where+    getHeistState     = templateState+    setHeistState s a = a { templateState = s }+++------------------------------------------------------------------------------+instance HasTimerState ApplicationState where+    getTimerState     = timerState+    setTimerState s a = a { timerState = s }+++------------------------------------------------------------------------------+-- | 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"+    timer <- timerInitializer+    return $ ApplicationState heist timer
+ project_template/default/src/Main.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE TemplateHaskell #-}++{-|++This is the entry point for this web server application.  It supports easily+switching between interpreting source and running statically compiled code.++In either mode, the generated program should be run from the root of the+project tree.  It locates its templates, static content, and source files in +development mode, relative to the current working directory when it is run.++When compiled without the production flag, only changes to the libraries, your+cabal file, or this file should require a recompile to be picked up.+Everything else is interpreted at runtime.  There are a few consequences of+this.++First, this is much slower.  Running the interpreter seems to take about+300ms, regardless of the simplicity of the loaded code.  The results of the+interpreter process are cached for a few seconds, to hopefully ensure that+the the interpreter is only invoked once for each load of a page and the+resources it depends on.++Second, the generated server binary is MUCH larger, since it links in the GHC+API (via the hint library).++Third, it results in initialization\/cleanup code defined by the @Initializer@+being called for each request.  This is to ensure that the current state is+compatible with the running action.  If your application state takes a long+time to load or clean up, the penalty will be visible.++Fourth, and the reason you would ever want to actually compile without+production mode, is that it enables a *much* faster development cycle. You can+simply edit a file, save your changes, and hit reload to see your changes+reflected immediately.++When this is compiled with the production flag, all the actions are statically+compiled in.  This results in much faster execution, a smaller binary size,+only running initialization and cleanup once per application run, and having+to recompile the server for any code change.++-}++module Main where++#ifdef PRODUCTION+import           Snap.Extension.Server+#else+import           Snap.Extension.Loader.Devel+import           Snap.Http.Server (quickHttpServe)+#endif++import           Application+import           Site++main :: IO ()+#ifdef PRODUCTION+main = quickHttpServe applicationInitializer site+#else+main = do+    snap <- $(loadSnapTH 'applicationInitializer 'site)+    quickHttpServe snap+#endif
+ project_template/default/src/Site.hs view
@@ -0,0 +1,56 @@+{-# LANGUAGE OverloadedStrings #-}++{-|++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.++-}++module Site+  ( site+  ) where++import           Control.Applicative+import           Data.Maybe+import           Snap.Extension.Heist+import           Snap.Extension.Timer+import           Snap.Util.FileServe+import           Snap.Types+import           Text.Templating.Heist++import           Application+++------------------------------------------------------------------------------+-- | Renders the front page of the sample site.+--+-- 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 = ifTop $ heistLocal (bindSplices indexSplices) $ render "index"+  where+    indexSplices = +        [ ("start-time",   startTimeSplice)+        , ("current-time", currentTimeSplice)+        ]+++------------------------------------------------------------------------------+-- | Renders the echo page.+echo :: Application ()+echo = do+    message <- decodedParam "stuff"+    heistLocal (bindString "message" message) $ render "echo"+  where+    decodedParam p = fromMaybe "" <$> getParam p+++------------------------------------------------------------------------------+-- | The main entry point handler.+site :: Application ()+site = route [ ("/",            index)+             , ("/echo/:stuff", echo)+             ]+       <|> fileServe "resources/static"
+ project_template/default/src/Snap/Extension/Timer.hs view
@@ -0,0 +1,54 @@+{-|++'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           Data.Text.Encoding+import           Snap.Types+import           Text.Templating.Heist+import           Text.XML.Expat.Tree hiding (Node)+++------------------------------------------------------------------------------+-- | 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 $ [mkText $ encodeUtf8 $ 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 $ [mkText $ encodeUtf8 $ T.pack $ show $ time]
+ project_template/default/src/Snap/Extension/Timer/Impl.hs view
@@ -0,0 +1,67 @@+{-# 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 you appliaction'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
@@ -0,0 +1,112 @@+name:           snap+version:        0.3.0+synopsis:       Snap: A Haskell Web Framework (Core)+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+maintainer:     snap@snapframework.com+build-type:     Simple+cabal-version:  >= 1.6+homepage:       http://snapframework.com/+category:       Web++extra-source-files:+  CONTRIBUTORS,+  LICENSE,+  README.md,+  README.SNAP.md,+  project_template/barebones/foo.cabal,+  project_template/barebones/src/Main.hs,+  project_template/default/foo.cabal,+  project_template/default/log/access.log,+  project_template/default/log/error.log,+  project_template/default/resources/static/screen.css,+  project_template/default/resources/templates/echo.tpl,+  project_template/default/resources/templates/index.tpl,+  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++Library+  hs-source-dirs: src++  exposed-modules:+    Snap.Extension.Heist,+    Snap.Extension.Heist.Impl,+    Snap.Extension.Loader.Devel,+    Snap.Extension.Server,+    Snap.Extension,+    Snap.Heist++  other-modules:+    Snap.Extension.Loader.Devel.Helper++  build-depends:+    base >= 4 && < 5,+    bytestring >= 0.9.1 && < 0.10,+    directory >= 1.0 && < 1.2,+    enumerator == 0.4.*,+    filepath >= 1.1 && <1.3,+    MonadCatchIO-transformers >= 0.2.1 && < 0.3,+    snap-core == 0.3.*,+    heist >= 0.4 && < 0.5,+    hint >= 0.3.3.1 && < 0.4,+    template-haskell >= 2.3 && < 2.6,+    time >= 1.0 && < 1.3++  if !os(windows) {+      build-depends:    unix >= 2.2.0.0 && < 2.5+  }++  if impl(ghc >= 6.12.0)+    ghc-options: -Wall -fwarn-tabs -funbox-strict-fields -O2+                 -fno-warn-orphans -fno-warn-unused-do-bind+  else+    ghc-options: -Wall -fwarn-tabs -funbox-strict-fields -O2+                 -fno-warn-orphans++Executable snap+  hs-source-dirs: src+  main-is: Snap/Starter.hs++  other-modules: Snap.StarterTH++  build-depends:+    attoparsec >= 0.8.0.2 && < 0.9,+    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.*,+    filepath >= 1.1 && <1.3,+    haskell98,+    mtl >= 2,+    old-locale,+    old-time,+    snap-core == 0.3.*,+    snap-server == 0.3.*,+    template-haskell >= 2.3 && < 2.6,+    text >= 0.11 && <0.12,+    time,+    unix-compat,+    zlib++  ghc-prof-options: -prof -auto-all++  if impl(ghc >= 6.12.0)+    ghc-options: -Wall -fwarn-tabs -funbox-strict-fields -O2+                 -fno-warn-orphans -fno-warn-unused-do-bind+  else+    ghc-options: -Wall -fwarn-tabs -funbox-strict-fields -O2+                 -fno-warn-orphans++source-repository head+  type:     git+  location: http://git.snapframework.com/snap.git
+ src/Snap/Extension.hs view
@@ -0,0 +1,502 @@+{-# 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+  , runInitializerHint+  , mkInitializer+  , defaultReloadHandler+  , nullReloadHandler+  ) where++import           Control.Applicative+import           Control.Exception (SomeException)+import           Control.Monad+import           Control.Monad.CatchIO+import           Control.Monad.Reader+import           Control.Monad.Trans+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)+import           Snap.Iteratee (enumBS, (>==>))+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, this is what you would do:++  @+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)++------------------------------------------------------------------------------+-- | Runs an initializer, obtains state, runs the handler, and tears everything+-- down all in one request. Used with the hint backend which reloads everything+-- every time.+runInitializerHint :: Initializer s+                   -- ^ The Initializer value+                   -> SnapExtend s ()+                   -- ^ An action in your application's monad.+                   -> Snap ()+runInitializerHint (Initializer r) (SnapExtend m) = do+    liftIO (r True) >>= either+                          -- Left s: no cleanup action+                          (\s -> runReaderT m s)+                          f+  where+    f (SCR s a _) = runReaderT m s `finally` liftIO a+++------------------------------------------------------------------------------+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 "Sucess" showE m, "\n"]+        msg           = mconcat $ map format ms+    finishWith $ setContentType "text/plain; charset=utf-8"+        $ setContentLength (fromIntegral $ B.length msg)+        $ modifyResponseBody (>==> enumBS 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 view
@@ -0,0 +1,70 @@+{-# 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           Snap.Types+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 ()++    -- | 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 = fmap rqPathInfo getRequest >>= render++    -- | 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, Splice n)]   -- ^ Splice mapping+  -> ByteString   -- ^ Template to render+  -> m ()+renderWithSplices sps t = heistLocal bsps $ render t+  where bsps = bindSplices sps
+ src/Snap/Extension/Heist/Impl.hs view
@@ -0,0 +1,197 @@+{-# 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 qualified Data.ByteString as B+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 you appliaction'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'. It takes one argument, a path to a+-- template directory containing @.tpl@ files.+heistInitializer :: MonadSnap m => FilePath -> Initializer (HeistState m)+heistInitializer p = do+    heistState <- liftIO $ do+        (origTs,sts) <- bindStaticTag $ 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+        (HeistState _ _ tsMVar _ modifier) <- asks getHeistState+        ts <- liftIO $ fmap modifier $ readMVar tsMVar+        renderTemplate ts t >>= maybe pass (\html -> do+            modifyResponse $ setContentType "text/html; charset=utf-8"+            modifyResponse $ setContentLength (fromIntegral $ B.length html)+            writeBS html)++    heistLocal f = local $ modifyHeistState $ \s ->+        s { _modifier = f . _modifier s }+++------------------------------------------------------------------------------+instance HasHeistState m s => MonadHeist m (ReaderT s m) where+    render t     = ReaderT $ \s -> do+        let (HeistState _ _ tsMVar _ modifier) = getHeistState s+        ts <- liftIO $ fmap modifier $ readMVar tsMVar+        renderTemplate ts t >>= maybe pass (\html -> do+            modifyResponse $ setContentType "text/html; charset=utf-8"+            modifyResponse $ setContentLength (fromIntegral $ B.length html)+            writeBS html)++    heistLocal f = local $ modifyHeistState $ \s ->+        s { _modifier = f . _modifier s }+++------------------------------------------------------------------------------+-- | 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'.+--+-- 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.+  -> [(B.ByteString, 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 view
@@ -0,0 +1,250 @@+{-# 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+  ) where++import           Data.List (groupBy, intercalate, isPrefixOf, nub)++import           Control.Concurrent (forkIO, myThreadId)+import           Control.Concurrent.MVar+import           Control.Exception+import           Control.Monad (when)+import           Control.Monad.Trans (liftIO)++import           Data.Maybe (catMaybes)+import           Data.Time.Clock++import           Language.Haskell.Interpreter hiding (lift, liftIO)+import           Language.Haskell.Interpreter.Unsafe++import           Language.Haskell.TH++import           Prelude hiding (catch)++import           System.Environment (getArgs)++------------------------------------------------------------------------------+import           Snap.Types+import           Snap.Extension (runInitializerHint)+import           Snap.Extension.Loader.Devel.Helper++------------------------------------------------------------------------------+-- | 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 () -> IO (Snap ())+--+-- with a magical implementation.+--+-- The returned Snap action runs the 'Initializer', runs the 'Snap' handler,+-- and does the cleanup.  This means that the whole application state will be+-- loaded and unloaded for each request.  To make this worthwhile, those steps+-- should be made quite fast.+--+-- 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 -> Q Exp+loadSnapTH initializer action = do+    args <- runIO getArgs++    let initMod = nameModule initializer+        initBase = nameBase initializer+        actMod = nameModule action+        actBase = nameBase action++        modules = catMaybes [initMod, actMod]+        opts = getHintOpts args++    let static = typecheck initializer action++    -- The let in this block causes the static expression to be+    -- pattern-matched, providing an extra check that the types were+    -- correct at compile-time, at least.+    [| let _ = $static :: IO (Snap ())+       in hintSnap opts modules initBase actBase |]+++-- Used to typecheck the initializer & action splices.+typecheck :: Name -> Name -> Q Exp+typecheck initializer action = do+    let [initE, actE] = map varE [initializer, action]+    [| return (runInitializerHint $initE $actE) |]+++------------------------------------------------------------------------------+-- | 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 = init' $ dropWhile (not . ("-package" `isPrefixOf`)) args+    copy = map (intercalate " ") . groupBy (\_ s -> not $ "-" `isPrefixOf` s)++    opts = hideAll ++ srcOpts ++ copy toCopy++    init' [] = []+    init' xs = init xs+++------------------------------------------------------------------------------+-- | 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.+--+-- This constructs an expression of type Snap (), that is essentially+--+-- > bracketSnap initialization cleanup handler+--+-- for the values of initialization, cleanup, and handler passed in.+--+-- 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   -- ^ The name of the initializer action+         -> String   -- ^ The name of the SnapExtend action+         -> IO (Snap ())+hintSnap opts modules initialization handler = do+    let action = intercalate " " [ "runInitializerHint"+                                 , initialization+                                 , handler+                                 ]+        interpreter = do+            loadModules . nub $ modules+            let imports = ["Prelude", "Snap.Types", "Snap.Extension"] ++ modules+            setImports . nub $ imports++            interpret action (as :: Snap ())++        loadInterpreter = unsafeRunInterpreterWithArgs opts interpreter++    -- Protect the interpreter from concurrent and high-speed serial+    -- access.+    loadAction <- protectedActionEvaluator 3 $ protectHandlers loadInterpreter++    return $ do+        interpreterResult <- liftIO loadAction+        case interpreterResult of+            Left err -> error $ format err+            Right handlerAction -> handlerAction+++------------------------------------------------------------------------------+-- | 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)+++------------------------------------------------------------------------------+-- | Create a wrapper for an action that protects the action from+-- concurrent or rapid evaluation.+--+-- There will be at least the passed-in 'NominalDiffTime' delay+-- between the finish of one execution of the action the start of the+-- next.  Concurrent calls to the wrapper, and calls within the delay+-- period, end up with the same calculated value returned.+--+-- 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 delay time has expired after the exception was raised.+protectedActionEvaluator :: NominalDiffTime -> IO a -> IO (IO a)+protectedActionEvaluator minReEval action = do+    -- The list of requesters waiting for a result.  Contains the+    -- ThreadId in case of exceptions, and an empty MVar awaiting a+    -- successful result.+    --+    -- type: MVar [(ThreadId, MVar a)]+    readerContainer <- newMVar []++    -- Contains the previous result, and the time it was stored, if a+    -- previous result has been computed.  The result stored is either+    -- the actual result, or the exception thrown by the calculation.+    --+    -- type: MVar (Maybe (Either SomeException a, UTCTime))+    resultContainer <- newMVar Nothing++    -- 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.+    return $ do+        existingResult <- readMVar resultContainer+        now <- getCurrentTime++        case existingResult of+            Just (res, ts) | diffUTCTime now ts < minReEval ->+                -- There's an existing result, and it's still valid+                case res of+                    Right val -> return val+                    Left  e   -> throwIO e+            _ -> 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, kick off evaluation of+                -- the action in a new thread. This is slightly+                -- careful to block asynchronous exceptions to that+                -- thread except when actually running the action.+                when (null readers) $ do+                    let runAndFill = block $ do+                            a <- unblock action+                            clearAndNotify (Right a) (flip putMVar a . snd)++                        killWaiting :: SomeException -> IO ()+                        killWaiting e = block $ do+                            clearAndNotify (Left e) (flip throwTo e . fst)+                            throwIO e++                        clearAndNotify r f = do+                            t <- getCurrentTime+                            _ <- swapMVar resultContainer $ Just (r, t)+                            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
+ src/Snap/Extension/Loader/Devel/Helper.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE CPP #-}+module Snap.Extension.Loader.Devel.Helper (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/Server.hs view
@@ -0,0 +1,153 @@+{-# 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           Snap.Http.Server (simpleHttpServe)+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 -> ConfigExtend s+completeConfig c = case getListen c' of+                    [] -> addListen (ListenHttp "0.0.0.0" 8000) c'+                    _ -> c'+  where c' = mappend defaultConfig c+++------------------------------------------------------------------------------+-- | 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+    (snap, cleanup) <- runInitializerWithReloadAction+                         verbose+                         initializer+                         (catch500 handler)+                         reloader+    let site = compress $ snap+    mapM_ printListen $ C.getListen config+    _   <- try $ serve $ site :: IO (Either SomeException ())+    putStr "\n"+    cleanup+    output "Shutting down..."++  where+    conf     = completeConfig config+    verbose  = fromJust $ getVerbose conf+    output   = when verbose . hPutStrLn stderr+    reloader = fromJust $ getReloadHandler conf+    compress = if fromJust $ getCompression conf then withCompression else id+    catch500 = flip catch $ fromJust $ getErrorHandler conf+    serve    = simpleHttpServe config++    listenToString (C.ListenHttp host port) =+        concat ["http://", fromUTF8 host, ":", show port, "/"]+    listenToString (C.ListenHttps host port _ _) =+        concat ["https://", fromUTF8 host, ":", show port, "/"]++    printListen l = output $ "Listening on " ++ listenToString l+++------------------------------------------------------------------------------+-- | 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/Heist.hs view
@@ -0,0 +1,44 @@+{-# LANGUAGE OverloadedStrings #-}+-- | This module contains convenience functions for helping render+-- Heist templates from Snap.+module Snap.Heist where++------------------------------------------------------------------------------+import           Control.Applicative+import           Data.ByteString.Char8 (ByteString)+import qualified Data.ByteString.Char8 as B+import           Snap.Types+import           Snap.Util.FileServe+import           Text.Templating.Heist+++------------------------------------------------------------------------------+-- | This is a convenience function.  It calls 'render' with the+-- content type set to @text/html; charset=utf-8@.+renderHtml :: (MonadSnap m) => TemplateState m -> ByteString -> m ()+renderHtml = render "text/html; charset=utf-8"+++------------------------------------------------------------------------------+-- | Renders a template with the provided content type.  If the+-- template cannot be loaded, 'pass' is called and the next handler is tried.+render :: (MonadSnap m)+       => ByteString      -- ^ the content type to include in the response+       -> TemplateState m -- ^ the TemplateState that contains the template+       -> ByteString      -- ^ the name of the template+       -> m ()+render contentType ts template = do+    bytes <- renderTemplate ts template+    flip (maybe pass) bytes $ \x -> do+        modifyResponse $ setContentType contentType+                       . setContentLength (fromIntegral $ B.length x)+        writeBS x+++------------------------------------------------------------------------------+-- | Handles the rendering of any template in TemplateState.+handleAllTemplates :: (MonadSnap m)+                   => TemplateState m -> m ()+handleAllTemplates ts =+    ifTop (renderHtml ts "index") <|>+    (renderHtml ts . B.pack =<< getSafePath)
+ src/Snap/Starter.hs view
@@ -0,0 +1,105 @@+{-# LANGUAGE TemplateHaskell #-}+module Main where++------------------------------------------------------------------------------+import           Char+import           Data.List+import qualified Data.ByteString.Char8 as S+import qualified Data.Text as T+import           Snap.Http.Server (snapServerVersion)+import           System+import           System.Directory+import           System.Console.GetOpt+import           System.FilePath+------------------------------------------------------------------------------++import Snap.StarterTH+++------------------------------------------------------------------------------+-- Creates a value tDir :: ([String], [(String, String)])+$(buildData "tDirBareBones" "barebones")+$(buildData "tDirDefault"   "default")++------------------------------------------------------------------------------+usage :: String+usage = unlines+    [ "Snap " ++ (S.unpack snapServerVersion) ++ " Project Kickstarter"+    , ""+    , "Usage:"+    , ""+    , "  snap <action>"+    , ""+    , "    <action> can be one of:"+    , "      init - create a new project directory structure in the " +++        "current directory"+    , ""+    , "  Note: you can use --help after any of the above actions to get help "+    , "  on that action"+    ]+++------------------------------------------------------------------------------+data InitFlag = InitBareBones+              | InitHelp+              | InitExtensions+  deriving (Show, Eq)+++setup :: String -> ([FilePath], [(String, String)]) -> IO ()+setup projName tDir = do+    mapM createDirectory (fst tDir)+    mapM_ write (snd tDir)+  where+    write (f,c) =+        if isSuffixOf "foo.cabal" f+          then writeFile (projName ++ ".cabal") (insertProjName $ T.pack c)+          else writeFile f c+    isNameChar c = isAlphaNum c || c == '-'+    insertProjName c = T.unpack $ T.replace+                           (T.pack "projname")+                           (T.pack $ filter isNameChar projName) c++------------------------------------------------------------------------------+initProject :: [String] -> IO ()+initProject args = do+    case getOpt Permute options args of+      (flags, _, [])+        | InitHelp `elem` flags -> do putStrLn initUsage+                                      exitFailure+        | otherwise             -> init' flags++      (_, _, 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)+                 "Print this message"+        , Option ['e'] ["extensions"] (NoArg InitExtensions)+                 "Depend on snap w/ extensions (default)"+        ]++    init' flags = 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+++------------------------------------------------------------------------------+main :: IO ()+main = do+    args <- getArgs+    case args of+        ("init":args') -> initProject args'+        _              -> do putStrLn usage+                             exitFailure
+ src/Snap/StarterTH.hs view
@@ -0,0 +1,57 @@+{-# LANGUAGE TemplateHaskell #-}+module Snap.StarterTH where++------------------------------------------------------------------------------+import qualified Data.Foldable as F+import           Data.List+import           Language.Haskell.TH+import           Language.Haskell.TH.Syntax+import           System.Directory.Tree+------------------------------------------------------------------------------+++------------------------------------------------------------------------------+-- Convenience types+type FileData = (String, String)+type DirData = FilePath+++------------------------------------------------------------------------------+-- Gets all the directorys in a DirTree+getDirs :: [FilePath] -> DirTree a -> [FilePath]+getDirs prefix (Dir n c) = (intercalate "/" (reverse (n:prefix))) : concatMap (getDirs (n:prefix)) c+getDirs _ (File _ _) = []+getDirs _ (Failed _ _) = []+++------------------------------------------------------------------------------+-- Reads a directory and returns a tuple of the list of all directories+-- encountered and a list of filenames and content strings.+readTree :: FilePath -> IO ([DirData], [FileData])+readTree dir = do+    d <- readDirectory $ dir ++ "/."+    let ps = zipPaths $ "" :/ (free d)+        fd = F.foldr (:) [] ps+        dirs = tail . getDirs [] $ free d++    return (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+    lift d+++------------------------------------------------------------------------------+-- Creates a declaration assigning the specified name the value returned by+-- dirQ.+buildData :: String -> FilePath -> Q [Dec]+buildData dirName tplDir = do+    let dir = mkName dirName++    typeSig <- SigD dir `fmap` [t| ([String], [(String, String)]) |]+    v <- valD (varP dir) (normalB $ dirQ tplDir) []+    return [typeSig, v]