snap 0.6.0 → 0.6.0.1
raw patch · 9 files changed
+416/−1 lines, 9 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
Files
- project_template/barebones/.ghci +4/−0
- project_template/barebones/log/access.log +0/−0
- project_template/default/.ghci +4/−0
- project_template/tutorial/.ghci +4/−0
- project_template/tutorial/foo.cabal +30/−0
- project_template/tutorial/log/placeholder +1/−0
- project_template/tutorial/src/Part2.lhs +14/−0
- project_template/tutorial/src/Tutorial.lhs +350/−0
- snap.cabal +9/−1
+ project_template/barebones/.ghci view
@@ -0,0 +1,4 @@+:set -isrc+:set -hide-package MonadCatchIO-mtl+:set -hide-package monads-fd+:set -XOverloadedStrings
+ project_template/barebones/log/access.log view
+ project_template/default/.ghci view
@@ -0,0 +1,4 @@+:set -isrc+:set -hide-package MonadCatchIO-mtl+:set -hide-package monads-fd+:set -XOverloadedStrings
+ project_template/tutorial/.ghci view
@@ -0,0 +1,4 @@+:set -isrc+:set -hide-package MonadCatchIO-mtl+:set -hide-package monads-fd+:set -XOverloadedStrings
+ project_template/tutorial/foo.cabal view
@@ -0,0 +1,30 @@+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: Tutorial.lhs++ Build-depends:+ base >= 4 && < 5,+ bytestring >= 0.9.1 && < 0.10,+ MonadCatchIO-transformers >= 0.2.1 && < 0.3,+ mtl >= 2 && < 3,+ snap == 0.6.*,+ snap-core == 0.6.*,+ snap-server == 0.6.*++ 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/tutorial/log/placeholder view
@@ -0,0 +1,1 @@+placeholder
+ project_template/tutorial/src/Part2.lhs view
@@ -0,0 +1,14 @@+> {-# LANGUAGE OverloadedStrings #-}+> module Part2 where++> import Snap.Snaplet++> data Foo = Foo+> +> data Bar = Bar+> +> fooInit = makeSnaplet "foo" "Foo snaplet" Nothing $ do+> return Foo+> +> barInit h = makeSnaplet "bar" "Bar snaplet" Nothing $ do+> return Bar
+ project_template/tutorial/src/Tutorial.lhs view
@@ -0,0 +1,350 @@+What Are Snaplets?+==================++A snaplet is a composable web application. Snaplets allow you to build+self-contained pieces of functionality and glue them together to make larger+applications. Here are some of the things provided by the snaplet API:++ - Infrastructure for application state/environment++ - Snaplet initialization, reload, and cleanup++ - Management of filesystem data and automatic snaplet installation++ - Unified config file infrastructure++One example might be a wiki snaplet. It would be distributed as a haskell+package that would be installed with cabal and would probably include code,+config files, HTML templates, stylesheets, JavaScript, images, etc. The+snaplet's code would provide the necessary API to let your application+interact seamlessly with the wiki functionality. When you run your+application for the first time, all of the wiki snaplet's filesystem resources+will automatically be copied into the appropriate places. Then you will+immediately be able to customize the wiki to fit your needs by editing config+files, providing your own stylesheets, etc. We will discuss this in more+detail later.++A snaplet can represent anything from backend Haskell infrastructure with no+user facing functionality to a small widget like a chat box that goes in the+corner of a web page to an entire standalone website like a blog or forum.+The possibilities are endless. A snaplet is a web application, and web+applications are snaplets. This means that using snaplets and writing+snaplets are almost the same thing, and it's trivial to drop a whole website+into another one.++We're really excited about the possibilities available with snaplets. In+fact, Snap already ships with snaplets for sessions, authentication, and+templating (with Heist), This gives you useful functionality out of the box,+and jump starts your own snaplet development by demonstrating some useful+design patterns. So without further ado, let's get started.++Snaplet Overview+================++The heart of the snaplets infrastructure is state management. Most nontrivial+pieces of a web app need some kind of state or environment data. Components+that do not need any kind of state or environment are probably more+appropriate as a standalone library than as a snaplet.++Before we continue, we must clarify an important point. The Snap web server+processes each request in its own green thread. This means that each request+will receive a separate copy of the state defined by your application and+snaplets, and modifications to that state only affect the local thread that+generates a single response. From now on, when we talk about state this is+what we are talking about. If you need global application state, you have to+use a thread-safe construct such as an MVar or IORef.++This post is written in literate Haskell, so first we need to get imports out+of the way.++> {-# LANGUAGE TemplateHaskell #-}+> {-# LANGUAGE OverloadedStrings #-}+> +> module Main where+> +> import Data.IORef+> import qualified Data.ByteString.Char8 as B+> import Data.Maybe+> import Snap+> import Snap.Snaplet.Heist+> import Part2++We start our application by defining a data structure to hold the state. This+data structure includes the state of all snaplets (wrapped in a Snaplet) used+by our application as well as any other state we might want.++> data App = App+> { _heist :: Snaplet (Heist App)+> , _foo :: Snaplet Foo+> , _bar :: Snaplet Bar+> , _companyName :: IORef B.ByteString+> }+>+> makeLenses [''App]++The field names begin with an underscore because of some more complicated+things going on under the hood. However, all you need to know right now is+that you should prefix things with an underscore and then call `makeLenses`.+This lets you use the names without an underscore in the rest of your+application.++The next thing we need to do is define an initializer.++> appInit :: SnapletInit App App+> appInit = makeSnaplet "myapp" "My example application" Nothing $ do+> hs <- nestSnaplet "heist" heist $ heistInit "templates"+> fs <- nestSnaplet "foo" foo $ fooInit+> bs <- nestSnaplet "" bar $ nameSnaplet "newname" $ barInit foo+> addRoutes [ ("/hello", writeText "hello world")+> , ("/fooname", with foo namePage)+> , ("/barname", with bar namePage)+> , ("/company", companyHandler)+> ]+> wrapHandlers (<|> heistServe)+> ref <- liftIO $ newIORef "fooCorp"+> return $ App hs fs bs ref++For now don't worry about all the details of this code. We'll work through the+individual pieces one at a time. The basic idea here is that to initialize an+application, we first initialize each of the snaplets, add some routes, run a+function wrapping all the routes, and return the resulting state data+structure. This example demonstrates the use of a few of the most common+snaplet functions.++nestSnaplet+-----------+ +All calls to child snaplet initializer functions must be wrapped in a call to+nestSnaplet. The first parameter is a URL path segment that is used to prefix+all routes defined by the snaplet. This lets you ensure that there will be no+problems with duplicate routes defined in different snaplets. If the foo+snaplet defines a route `/foopage`, then in the above example, that page will+be available at `/foo/foopage`. Sometimes though, you might want a snaplet's+routes to be available at the top level. To do that, just pass an empty string+to nestSnaplet as shown above with the bar snaplet.++In our example above, the bar snaplet does something that needs to know about+the foo snaplet. Maybe foo is a database snaplet and bar wants to store or+read something. In order to make that happen, it needs to have a "handle" to+the snaplet. Our handles are whatever field names we used in the App data+structure minus the initial underscore character. They are automatically+generated by the `makeLenses` function. For now it's sufficient to think of+them as a getter and a setter combined (to use an OO metaphor).++The second parameter to nestSnaplet is the lens to the snaplet you're nesting.+In order to place a piece into the puzzle, you need to know where it goes.++nameSnaplet+-----------++The author of a snaplet defines a default name for the snaplet in the first+argument to the makeSnaplet function. This name is used for the snaplet's+directory in the filesystem. If you don't want to use the default name, you+can override it with the `nameSnaplet` function. Also, if you want to have two+instances of the same snaplet, then you will need to use `nameSnaplet` to give+at least one of them a unique name.++addRoutes+---------++The `addRoutes` function is how an application (or snaplet) defines its+routes. Under the hood the snaplet infrastructure merges all the routes from+all snaplets, prepends prefixes from `nestSnaplet` calls, and passes the list+to Snap's+[route](http://hackage.haskell.org/packages/archive/snap-core/0.5.1.4/doc/html/Snap-Types.html#v:route)+function.++A route is a tuple of a URL and a handler function that will be called when+the URL is requested. Handler is a wrapper around the Snap monad that handles+the snaplet's infrastructure. During initialization, snaplets use the+`Initializer` monad. During runtime, they use the `Handler` monad. We'll+discuss `Handler` in more detail later. If you're familiar with Snap's old+extension system, you can think of it as roughly equivalent to the Application+monad. It has a `MonadState` instance that lets you access and modify the+current snaplet's state, and a `MonadSnap` instance providing the+request-processing functions defined in Snap.Types.++wrapHandlers+------------++`wrapHandlers` allows you to apply an arbitrary `Handler` transformation to+the top-level handler. This is useful if you want to do some generic+processing at the beginning or end of every request. For instance, a session+snaplet might use it to touch a session activity token before routing happens.+It could also be used to implement custom logging. The example above uses it+to define heistServe (provided by the Heist snaplet) as the default handler to+be tried if no other handler matched. This may seem like an easy way to define+routes, but if you string them all together in this way each handler will be+evaluated sequentially and you'll get O(n) time complexity, whereas routes+defined with `addRoutes` have O(log n) time complexity. Therefore, in a+real-world application you would probably want to have `("", heistServe)` in+the list passed to `addRoutes`.++with+----++The last unfamiliar function in the example is `with`. Here it accompanies a+call to the function `namePage`. `namePage` is a simple example handler and+looks like this.++> namePage :: Handler b v ()+> namePage = do+> mname <- getSnapletName+> writeText $ fromMaybe "This shouldn't happen" mname++This function is a generic handler that gets the name of the current snaplet+and writes it into the response with the `writeText` function defined by the+snap-core project. The type variables 'b' and 'v' indicate that this function+will work in any snaplet with any base application. The 'with' function is+used to run `namePage` in the context of the snaplets foo and bar for the+corresponding routes. ++Site Reloading+--------------++Snaplet Initializers serve dual purpose as both initializers and reloaders.+Reloads are triggered by a special handler that is bound to the+`/admin/reload` route. This handler re-runs the site initializer and if it is+successful, loads the newly generated in-memory state. To prevent denial of+service attacks, the reload route is only accessible from localhost.++If there are any errors during reload, you would naturally want to see them in+the HTTP response returned by the server. However, when these same+initializers are run when you first start your app, you will want to see+status messages printed to the console. To make this possible we provide the+`printInfo` function. You should use it to output any informational messages+generated by your initializers. If you print directly to standard output or+standard error, then those messages will not be available in your browser when+you reload the site.++Working with state+------------------++`Handler b v` has a `MonadState v` instance. This means that you can access+all your snaplet state through the get, put, gets, and modify functions that+are probably familiar from the state monad. In our example application we+demonstrate this with `companyHandler`.++> companyHandler :: Handler App App ()+> companyHandler = method GET getter <|> method POST setter+> where+> getter = do+> nameRef <- gets _companyName+> name <- liftIO $ readIORef nameRef+> writeBS name+> setter = do+> mname <- getParam "name"+> nameRef <- gets _companyName+> liftIO $ maybe (return ()) (writeIORef nameRef) mname+> getter++If you set a GET request to `/company`, you'll get the string "fooCorp" back.+If you send a POST request, it will set the IORef held in the `_companyName`+field in the `App` data structure to the value of the `name` field. Then it+calls the getter to return that value back to you so you can see it was+actually changed. Again, remember that this change only persists across+requests because we used an IORef. If `_companyName` was just a plain string+and we had used modify, the changed result would only be visible in the rest+of the processing for that request.++The Heist Snaplet+=================++The astute reader might ask why there is no `with heist` in front of the call+to `heistServe`. And indeed, that would normally be the case. But we decided+that an application will never need more than one instance of a Heist snaplet.+So we provided a type class called `HasHeist` that allows an application to+define the global reference to its Heist snaplet by writing a `HasHeist`+instance. In this example we define the instance as follows:++> instance HasHeist App where heistLens = subSnaplet heist++Now all we need is a simple main function to serve our application.++> main :: IO ()+> main = serveSnaplet defaultConfig appInit++This completes a full working application. We did leave out a little dummy+code for the Foo and Bar snaplets. This code is included in Part2.hs. For+more information look in our API documentation. No really, that wasn't a+joke. The API docs are written as prose. It is written to be very easy to+read, while having the benefit of including all the actual type signatures.++Filesystem Data and Automatic Installation+==========================================++Some snaplets will have data stored in the filesystem that should be installed+into the directory of any project that uses it. Here's an example of what a+snaplet filesystem layout might look like:++ foosnaplet/+ |-- *snaplet.cfg*+ |-- db.cfg+ |-- public/+ |-- stylesheets/+ |-- images/+ |-- js/+ |-- *snaplets/*+ |-- subsnaplet1/+ |-- subsnaplet2/+ |-- templates/++Only the starred items are actually enforced by current code, but we want to+establish the others as a convention. The file snaplet.cfg is automatically+read by the snaplet infrastructure. It is available to you via the+`getSnapletUserConfig` function. Config files use the format defined by Bryan+O'Sullivan's excellent [configurator+package](http://hackage.haskell.org/package/configurator). In this example,+the user has chosen to put db config items in a separate file and use+configurator's import functionality to include it in snaplet.cfg. If+foosnaplet uses `nestSnaplet` or `embedSnaplet` to include any other snaplets,+then filesystem data defined by those snaplets will be included in+subdirectories under the `snaplets/` directory.++So how do you tell the snaplet infrastructure that your snaplet has filesystem+data that should be installed? Look at the definition of appInit above. The+third argument to the makeSnaplet function is where we specify the filesystem+directory that should be installed. That argument has the type `Maybe (IO+FilePath)`. In this case we used `Nothing` because our simple example doesn't+have any filesystem data. As an example, let's say you are creating a snaplet+called killerapp that will be distributed as a hackage project called+snaplet-killerapp. Your project directory structure will look something like+this:++ snaplet-killerapp/+ |-- resources/+ |-- snaplet-killerapp.cabal+ |-- src/++All of the files and directories listed above under foosnaplet/ will be in+resources/. Somewhere in the code you will define an initializer for the+snaplet that will look like this:++ killerInit = makeSnaplet "killerapp" "42" (Just dataDir) $ do++The primary function of Cabal is to install code. But it has the ability to+install data files and provides a function called `getDataDir` for retrieving+the location of these files. Since it returns a different result depending on+what machine you're using, the third argument to `makeSnaplet` has to be `Maybe+(IO FilePath)` instead of the more natural pure version. To make things more+organized, we use the convention of putting all your snaplet's data files in a+subdirectory called resources. So we need to create a small function that+appends `/resources` to the result of `getDataDir`.++ import Paths_snaplet_killerapp+ dataDir = liftM (++"/resources") getDataDir++If our project is named snaplet-killerapp, the `getDataDir` function is+defined in the module Paths_snaplet_killerapp, which we have to import. To+make everything work, you have to tell Cabal about your data files by+including a section like the following in snaplet-killerapp.cabal:++ data-files:+ resources/snaplet.cfg,+ resources/public/stylesheets/style.css,+ resources/templates/page.tpl++Now whenever your snaplet is used, its filesystem data will be automagically+copied into the local project that is using it, whenever the application is+run and it sees that the files don't already exist.+
snap.cabal view
@@ -1,5 +1,5 @@ name: snap-version: 0.6.0+version: 0.6.0.1 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@@ -16,8 +16,11 @@ LICENSE, README.md, README.SNAP.md,+ project_template/barebones/.ghci, project_template/barebones/foo.cabal,+ project_template/barebones/log/access.log, project_template/barebones/src/Main.hs,+ project_template/default/.ghci, project_template/default/foo.cabal, project_template/default/log/access.log, project_template/default/log/error.log,@@ -27,6 +30,11 @@ project_template/default/src/Application.hs, project_template/default/src/Main.hs, project_template/default/src/Site.hs,+ project_template/tutorial/.ghci,+ project_template/tutorial/foo.cabal,+ project_template/tutorial/log/placeholder,+ project_template/tutorial/src/Part2.lhs,+ project_template/tutorial/src/Tutorial.lhs, extra/hscolour.css, extra/haddock.css, extra/logo.gif,