snap-web-routes 0.4.0.0 → 0.5.0.0
raw patch · 8 files changed
+241/−72 lines, 8 filesdep +bytestringPVP ok
version bump matches the API change (PVP)
Dependencies added: bytestring
API changes (from Hackage documentation)
- Snap.Snaplet.Router.Heist: urlParamsSplice :: (HasRouter m, PathInfo (URL m)) => URL m -> [(Text, Maybe Text)] -> m [Node]
- Snap.Snaplet.Router.Heist: urlSplice :: (HasRouter m, PathInfo (URL m)) => URL m -> m [Node]
+ Snap.Snaplet.Router.HeistSplices: urlParamsSplice :: (HasRouter m, PathInfo (URL m)) => URL m -> [(Text, Maybe Text)] -> m [Node]
+ Snap.Snaplet.Router.HeistSplices: urlSplice :: (HasRouter m, PathInfo (URL m)) => URL m -> m [Node]
+ Snap.Snaplet.Router.REST: Edit :: id -> Resource id
+ Snap.Snaplet.Router.REST: EditS :: SingletonResource
+ Snap.Snaplet.Router.REST: Index :: Resource id
+ Snap.Snaplet.Router.REST: New :: Resource id
+ Snap.Snaplet.Router.REST: NewS :: SingletonResource
+ Snap.Snaplet.Router.REST: Show :: id -> Resource id
+ Snap.Snaplet.Router.REST: ShowS :: SingletonResource
+ Snap.Snaplet.Router.REST: data PathInfo id => Resource id
+ Snap.Snaplet.Router.REST: data SingletonResource
+ Snap.Snaplet.Router.REST: instance (Eq id, PathInfo id) => Eq (Resource id)
+ Snap.Snaplet.Router.REST: instance (Read id, PathInfo id) => Read (Resource id)
+ Snap.Snaplet.Router.REST: instance (Show id, PathInfo id) => Show (Resource id)
+ Snap.Snaplet.Router.REST: instance Eq SingletonResource
+ Snap.Snaplet.Router.REST: instance PathInfo SingletonResource
+ Snap.Snaplet.Router.REST: instance PathInfo id => PathInfo (Resource id)
+ Snap.Snaplet.Router.REST: instance Read SingletonResource
+ Snap.Snaplet.Router.REST: instance Show SingletonResource
Files
- README.md +74/−25
- changelog +3/−0
- snap-web-routes.cabal +29/−6
- src/Snap/Snaplet/Router.hs +19/−7
- src/Snap/Snaplet/Router/Heist.hs +0/−32
- src/Snap/Snaplet/Router/HeistSplices.hs +32/−0
- src/Snap/Snaplet/Router/REST.hs +83/−0
- src/Snap/Snaplet/Router/URL.hs +1/−2
README.md view
@@ -2,15 +2,21 @@ ## Type safe URLs for Snap -[`Snap web routes`](http://hackage.haskell.org/package/snap-web-routes) provides type safe URLs for Snap using [`web routes`](http://hackage.haskell.org/package/web-routes).+[`Snap web routes`](http://hackage.haskell.org/package/snap-web-routes)+provides type safe URLs for Snap using+[`web routes`](http://hackage.haskell.org/package/web-routes). ## How to use -The tutorial assumes you have a standard Snap app layout with an Application.hs and Site.hs. If your setup differs you'll need to adapt the instructions accordingly.+The tutorial assumes you have a standard Snap app layout with an+Application.hs and Site.hs. If your setup differs you'll need to adapt+things accordingly. ### `Application.hs` -To get going, you'll need to add a few things to `Application.hs`. This includes creating the URL data type and adding the routing function to our `App` data type.+To get going, you'll need to add a few things to `Application.hs`.+This includes creating the URL data type and adding the routing+function to our `App` data type. ```haskell -- Enable a few extensions@@ -27,15 +33,17 @@ -- Snap.Snaplet.Router.Types exports everything you need to -- define your PathInfo and HasRouter instances.-import Snap.Web.Routes.Types+import Snap.Snaplet.Router.Types -- Your URL data type. Deriving a `Generic` allows you to -- get a free `PathInfo` instance. data AppUrl- = Count Int+ = Login+ | Logout+ | Count Int | Echo Text | Paths [Text]- deriving (Generic)+ deriving (Eq, Show, Read, Generic) -- Extend your App type to include the router snaplet. data App = App@@ -49,18 +57,17 @@ instance PathInfo AppUrl -- You need to define a HasRouter instance for your app.--- @type URL (Handler App App)@ must be set to the URL--- data type you defined above.--- @with router@ uses the lens for the @RouterState@ snaplet+-- You must set type URL (Handler App App) to the URL+-- data type you defined above. The router in+-- `with router` is the lens for the @RouterState@ snaplet -- you added to App. instance HasRouter (Handler App App) where type URL (Handler App App) = AppUrl getRouterState = with router get -- You also need to define a HasRouter instance for the--- router snaplet.--- @type URL (Handler b RouterState)@ must be set to the URL--- data type you defined above.+-- router snaplet. Once again, set type URL (Handler b+-- RouterState) to the data type you defined above. instance HasRouter (Handler b RouterState) where type URL (Handler b RouterState) = AppUrl getRouterState = get@@ -68,7 +75,8 @@ ### `Site.hs` -Moving on to `Site.hs`, we'll setup handlers for each URL, as well initialise our app with the router snaplet..+Moving on to `Site.hs`, we'll setup handlers for each URL, as well as initialise+our app with the router snaplet.. ```haskell -- Snap.Snaplet.Router provides routing functions@@ -84,6 +92,8 @@ routeAppUrl :: AppUrl -> Handler App App () routeAppUrl appUrl = case appUrl of+ (Login) -> with auth handleLoginSubmit+ (Logout) -> with auth handleLogout (Count n) -> writeText $ ("Count = " `T.append` (T.pack $ show n)) (Echo text) -> echo text (Paths ps) -> writeText $ T.intercalate " " ps@@ -103,27 +113,66 @@ return $ App h r ``` -The prefix you pass to the router snaplet must match the prefix you specified in routes, e.g. if it was `("/prefix", routeWith routeAppUrl)`) then:+The prefix you pass to the router snaplet must match the prefix you specified+in routes, e.g. if it was `("/prefix", routeWith routeAppUrl)`) then: ```haskell r <- nestSnaplet "router" router $ initRouter "/prefix" ``` -If you are having trouble figuring out why a particular request isn't routing as expected, try replacing `routeWith` with `routeWithDebug`. It'll display the available routes, as well as any failed route parses. Just remember that it's **not suitable for production** use, and only displays debugging information for local requests.+If you are having trouble figuring out why a particular request isn't routing+as expected, try replacing `routeWith` with `routeWithDebug`. It'll display+the available routes, as well as any failed route parses. Just remember that+it's **not suitable for production** use, and only displays debugging+information for local requests. -### Rendering URLs+### Using URLs -Helper functions are provided for rendering URLs:+Let's look at how you can use your newly defined URL data type in your app.+Firstly, you'll probably want to add links in Heist views. This is easily+accomplished with the `urlSplice` and `urlParamsSplice` functions. ```haskell-echo :: T.Text -> Handler App App ()-echo msg = do- if msg == "test" then redirectURL (Echo "test passed") else renderEcho+linksHandler :: Handler App App ()+linksHandler = heistLocal (I.bindSplices linksSplices) $ render "links" where- renderEcho = heistLocal (I.bindSplices echoSplices) $ render "echo"- echoSplices = do- "message" ## I.textSplice msg- "countUrl" ## urlSplice (Count 10)+ linksSplices = do+ "loginUrl" ## urlSplice Login+ "echoUrl" ## urlSplice (Echo "ping")+ "countUrl" ## urlParamsSplice (Count 10) [("explanation", Just "true")] ``` -In the example above you'll find we use `urlSplice` to turn a URL into a splice, and `redirectURL` to redirect to a URL. There is also `urlPath` to render a URL as Text, as well as params versions of all these functions (`urlParamsSplice`, `redirectURLParams` and `urlPathParams`) that take a params list to append as a query string.+As you can see, splicing URLs into Heist views is easily accomplished. You+will likely also want to redirect to the handler for a certain URL. To do+this we've got `redirectURL` and `redirectURLParams`. Let's look at an+example.++```haskell+doSomethingHandler :: Handler App App ()+doSomethingHandler = doSomething >> redirectURL Logout+```++However, you will sometimes wish to redirect within a handler that runs in a+snaplet other than the main app. With the router snaplet though, this is+easily done:++```haskell+handleLogout :: Handler App (AuthManager App) ()+handleLogout = logout >> (withTop router $ redirectURL (Echo "logged out"))+```++Lastly, you can render a URL as Text with `urlPath` and `urlPathParams`.++```haskell+messageHandler :: Handler App App ()+messageHandler = do+ pathText <- urlPath (Echo "hello")+ heistLocal (I.bindSplices $ messageSplices path) $ render "message"+ where+ messageSplices path = do+ "message" ## I.textSplice $ "The path is " `append` pathText+```++Remember, for each of `urlSplice`, `redirectURL` and `urlPath` there is a+params version that takes a params list as an extra argument, and renders+the URL with the given params as a query string.
changelog view
@@ -1,3 +1,6 @@+* Add Resource and SingletonResource data types for RESTful routes+* Ignore trailing slashes+ 2014-05-07 0.4.0.0 * Make router a snaplet
snap-web-routes.cabal view
@@ -1,13 +1,34 @@ name: snap-web-routes-version: 0.4.0.0+version: 0.5.0.0 synopsis: Type safe URLs for Snap description:- Type safe URL generation and routing for Snap using web-routes.+ Type safe URL generation and routing for Snap using <http://hackage.haskell.org/package/web-routes web-routes>, and builds on <https://github.com/stepcut/snap-web-routes-demo work>+ done by Jeremy Shaw. .- To get started, there is a __<https://github.com/lukerandall/snap-web-routes/blob/master/README.md tutorial on GitHub>__.+ Get started with the comprehensive __<https://github.com/lukerandall/snap-web-routes/blob/master/README.md tutorial>__. .- This builds on <https://github.com/stepcut/snap-web-routes-demo work>- done by Jeremy Shaw. Thanks Jeremy!+ = Brief overview+ .+ It allows you to define a data type that represents the routes in your application:+ .+ > data AppUrl+ > = Login -- routes to /login+ > | Logout -- routes to /logout+ > | User (Resource UserId) -- provides RESTful routes at /user+ .+ 'Resource' is documented in Snap.Snaplet.Router.REST, and makes defining RESTful routes easier. Also provided are functions to use the URL data type in your app:+ .+ > someHandler :: Handler App App ()+ > someHandler :: doSomething >> redirectURL $ User Index+ .+ and to generate URLs in views:+ .+ > linksHandler :: Handler App App ()+ > linksHandler = heistLocal (I.bindSplices linksSplices) $ render "links"+ > where+ > linksSplices = do+ > "loginUrl" ## urlSplice Login+ . homepage: https://github.com/lukerandall/snap-web-routes bug-reports: https://github.com/lukerandall/snap-web-routes/issues@@ -30,13 +51,15 @@ exposed-modules: Snap.Snaplet.Router- Snap.Snaplet.Router.Heist+ Snap.Snaplet.Router.HeistSplices Snap.Snaplet.Router.Internal.Types+ Snap.Snaplet.Router.REST Snap.Snaplet.Router.Types Snap.Snaplet.Router.URL build-depends: base >= 4.4 && < 5,+ bytestring >= 0.9.1 && < 0.11, heist >= 0.13 && < 0.14, mtl >= 2 && < 3, snap-core >= 0.9 && < 0.11,
src/Snap/Snaplet/Router.hs view
@@ -18,18 +18,19 @@ ) where +import Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as B import Data.Text import Snap.Core (MonadSnap) import qualified Snap.Core as SC import Snap.Snaplet-import Web.Routes (PathInfo, toPathInfoParams, fromPathInfo)+import Web.Routes (PathInfo, fromPathInfo) -------------------------------------------------------------------------------import Snap.Snaplet.Router.Heist+import Snap.Snaplet.Router.HeistSplices import Snap.Snaplet.Router.URL import Snap.Snaplet.Router.Internal.Types - ------------------------------------------------------------------------------ -- | Snaplet initializer. initRouter :: Text -- ^ Prefix to add to paths.@@ -61,8 +62,19 @@ ------------------------------------------------------------------------------ routeWithOr :: (PathInfo url, MonadSnap m) => (url -> m ()) -> (Text -> m ()) -> m ()-routeWithOr router leftFn =- do rq <- SC.getRequest- case fromPathInfo $ SC.rqPathInfo rq of- (Left e) -> leftFn . pack $ e+routeWithOr router onLeft = do+ rqPath <- rqPathInfoNormalized+ case fromPathInfo rqPath of+ (Left e) -> onLeft . pack $ e (Right url) -> router url++rqPathInfoNormalized+ :: (MonadSnap m) => m ByteString+rqPathInfoNormalized = do+ rq <- SC.getRequest+ return . dropTrailingSlashes $ SC.rqPathInfo rq+ where+ dropTrailingSlashes s =+ if B.singleton '/' `B.isSuffixOf` s+ then dropTrailingSlashes $ B.init s+ else s
− src/Snap/Snaplet/Router/Heist.hs
@@ -1,32 +0,0 @@-{-# LANGUAGE FlexibleContexts #-}--module Snap.Snaplet.Router.Heist- ( urlSplice- , urlParamsSplice- ) where---import Data.Text-import Snap.Snaplet.Router.Internal.Types-import Snap.Snaplet.Router.URL (urlPathParams)-import Text.XmlHtml hiding (render)-import Web.Routes (PathInfo)------------------------------------------------------------------------------------ | Returns the given URL as a `Heist` splice-urlSplice- :: (HasRouter m, PathInfo (URL m)) => URL m -- ^ URL data type- -> m [Node]-urlSplice u = urlParamsSplice u []------------------------------------------------------------------------------------ | Returns the given URL as a `Heist` splice with query string for params-urlParamsSplice- :: (HasRouter m, PathInfo (URL m)) => URL m -- ^ URL data type- -> [(Text, Maybe Text)] -- ^ Params- -> m [Node]-urlParamsSplice u p = do- t <- urlPathParams u p- return [TextNode t]
+ src/Snap/Snaplet/Router/HeistSplices.hs view
@@ -0,0 +1,32 @@+{-# LANGUAGE FlexibleContexts #-}++module Snap.Snaplet.Router.HeistSplices+ ( urlSplice+ , urlParamsSplice+ ) where+++import Data.Text+import Snap.Snaplet.Router.Internal.Types+import Snap.Snaplet.Router.URL (urlPathParams)+import Text.XmlHtml hiding (render)+import Web.Routes (PathInfo)+++------------------------------------------------------------------------------+-- | Returns the given URL as a `Heist` splice+urlSplice+ :: (HasRouter m, PathInfo (URL m)) => URL m -- ^ URL data type+ -> m [Node]+urlSplice u = urlParamsSplice u []+++------------------------------------------------------------------------------+-- | Returns the given URL as a `Heist` splice with query string for params+urlParamsSplice+ :: (HasRouter m, PathInfo (URL m)) => URL m -- ^ URL data type+ -> [(Text, Maybe Text)] -- ^ Params+ -> m [Node]+urlParamsSplice u p = do+ t <- urlPathParams u p+ return [TextNode t]
+ src/Snap/Snaplet/Router/REST.hs view
@@ -0,0 +1,83 @@+-- | This module provides data types useful for declaring RESTful routes.+-- 'Resource' is provided for the common case of resource specified+-- by a unique identifier (for e.g. @\/products\/3@). 'SingletonResource'+-- is provided for resources that do not require an identifier. Typical+-- usage would be as follows:+--+-- @+-- data AppUrl+-- = User (Resource Int)+-- | Profile SingletonResource+-- | Login+-- | Logout+-- @+--+-- This now allows you to define handlers for different end points for+-- your resource.+-- @+-- routeAppUrl appUrl =+-- case appUrl of+-- Login -> with auth handleLogin -- \/login+-- Logout -> with auth handleLogout -- \/logout+-- User Index -> handleUserIndex -- \/user+-- User New -> handleUserNew -- \/user\/new+-- User (Show n) -> handleUserShow n -- \/user\/:Int+-- User (Edit n) -> handleUserEdit n -- \/user\/:Int\/edit+-- Profile ShowS -> handleProfileShow -- \/profile+-- Profile NewS -> handleProfileNew -- \/profile/new+-- Profile EditS -> handleProfileEdit -- \/profile/edit+-- @+--++{-# LANGUAGE OverloadedStrings #-}++module Snap.Snaplet.Router.REST+ ( Resource (..)+ , SingletonResource (..)+ ) where+++import Control.Applicative+import Web.Routes.PathInfo (PathInfo (..), segment, toPathSegments, fromPathSegments)+++-- | A data type to represent RESTful resources that have a unique identifier.+-- The data type used as the identifier must be an instance of 'PathInfo'.+data (PathInfo id) => Resource id+ = Index+ | New+ | Show id+ | Edit id+ deriving (Eq, Show, Read)+++-- | A data type to represent singleton RESTful resources. Generally these+-- are identified in some other way, often the user's session.+data SingletonResource+ = ShowS+ | NewS+ | EditS+ deriving (Eq, Show, Read)+++instance (PathInfo id) => PathInfo (Resource id) where+ toPathSegments Index = []+ toPathSegments New = ["new"]+ toPathSegments (Show n) = toPathSegments n+ toPathSegments (Edit n) = toPathSegments n ++ ["edit"]++ fromPathSegments =+ New <$ segment "new"+ <|> fromPathSegments <**> ((pure Edit <* segment "edit") <|> pure Show)+ <|> pure Index+++instance PathInfo SingletonResource where+ toPathSegments ShowS = []+ toPathSegments NewS = ["new"]+ toPathSegments EditS = ["edit"]++ fromPathSegments =+ NewS <$ segment "new"+ <|> EditS <$ segment "edit"+ <|> pure ShowS
src/Snap/Snaplet/Router/URL.hs view
@@ -12,9 +12,8 @@ import Data.Text.Encoding (encodeUtf8) import Snap.Core (MonadSnap) import qualified Snap.Core as SC-import Snap.Snaplet import Snap.Snaplet.Router.Internal.Types-import Web.Routes (PathInfo, toPathInfoParams, fromPathInfo)+import Web.Routes (PathInfo, toPathInfoParams) ------------------------------------------------------------------------------