wai-middleware-route-0.7.0: src/Network/Wai/Middleware/Route.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE BangPatterns #-}
-- | This module contains helpers for use "Yesod.Routes.Dispatch" with
-- "Network.Wai".
--
-- This 'Middleware' uses first 'D.Piece' in path to route @HTTP@ method.
-- 'D.Static' means concrete method. 'D.Dynamic' means any method.
module Network.Wai.Middleware.Route (
-- * Routing rules
Rule(..),
mkRoutes,
mkRoutes',
mkRoute,
-- * Middleware
dispatch,
dispatch_
) where
import Control.Applicative ((<$>), (<*>))
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8)
import Network.Wai (Application, requestMethod, pathInfo)
import qualified Yesod.Routes.Dispatch as D
-- | Rule for route. Rules without single quotes (@'@) means fixed length
-- paths. And vice versa, rules with single quotes (@'@) means paths with
-- variable lengh
--
-- Paths converts to 'D.Piece's by following rules:
--
-- * Paths splits by slashes (@/@).
--
-- * Text between slashes becomes 'D.Static' 'D.Piece'. The same thing
-- happens with the text at the ends of paths.
--
-- * Hashes (@#@) inside slashes becomes 'D.Dynamic' 'D.Piece's.
--
-- * To make route with variable length just add asterisk (@\*@) after last
-- slash.
--
-- > "foo"
-- > [Static "foo"] Fixed
-- >
-- > "foo/bar"
-- > [Static "foo", Static "bar"] Fixed
-- >
-- > "foo/#/bar"
-- > [Static "foo", Dynamic, Static "bar"] Fixed
-- >
-- > "foo/#/bar/baz/*"
-- > [Dynamic, Static "foo", Dynamic, Static "bar", Static "baz"] Variable
data Rule =
Get T.Text Application -- ^ @GET@ method
| Post T.Text Application -- ^ @POST@ method
| Head T.Text Application -- ^ @HEAD@ method
| Put T.Text Application -- ^ @PUT@ method
| Delete T.Text Application -- ^ @DELETE@ method
| Trace T.Text Application -- ^ @TRACE@ method
| Connect T.Text Application -- ^ @CONNECT@ method
| Options T.Text Application -- ^ @OPTIONS@ method
| Any T.Text Application -- ^ Any @HTTP@ method
| Gen T.Text T.Text Application
-- ^ Generic rule with @HTTP@ method and path
-- | Make 'D.Route's from 'Rules'.
--
-- Equivalent @map mkRoute@
mkRoutes ::
[Rule]
-- ^ Routing rules
-> [D.Route Application]
mkRoutes = map mkRoute
{-# INLINE mkRoutes #-}
-- | Make 'D.Dispatch's from 'Rules'.
--
-- Equivalent @toDispatch . mkRoutes@
mkRoutes' :: [Rule] -> D.Dispatch Application
mkRoutes' = D.toDispatch . mkRoutes
-- | Make 'D.Route' from 'Rule'. 'D.rhPieces' of 'D.Route' will be
-- prepended with 'D.Piece' with corresponding @HTTP@ method.
-- 'D.Static' means concrete method. 'D.Dynamic' means any method.
--
-- > mkRoute $ Get "foo/bar" app
-- > Route [Static "foo", Static "bar"] False (const $ Just app)
mkRoute :: Rule -> D.Route Application
mkRoute (Get p a) = mkGenRoute (D.Static "GET") p a
mkRoute (Post p a) = mkGenRoute (D.Static "POST") p a
mkRoute (Head p a) = mkGenRoute (D.Static "HEAD") p a
mkRoute (Put p a) = mkGenRoute (D.Static "PUT") p a
mkRoute (Delete p a) = mkGenRoute (D.Static "DELETE") p a
mkRoute (Trace p a) = mkGenRoute (D.Static "TRACE") p a
mkRoute (Connect p a) = mkGenRoute (D.Static "CONNECT") p a
mkRoute (Options p a) = mkGenRoute (D.Static "OPTIONS") p a
mkRoute (Any p a) = mkGenRoute D.Dynamic p a
mkRoute (Gen m p a) = mkGenRoute (D.Static m) p a
{-# INLINE mkRoute #-}
-- | Make generic route
mkGenRoute ::
D.Piece -- ^ Method piece. 'D.Dynamic' means any method.
-> T.Text -- ^ Path pieces
-> Application -- ^ Routed application
-> D.Route Application
mkGenRoute m path =
let (!pieces, !hasMulti) = mkPieces path in
D.Route (m:pieces) hasMulti . const . Just
{-# INLINE mkGenRoute #-}
-- | Parse 'T.Text' and make tuple with 'D.Piece's and 'D.hasMulti'.
--
-- > "" -- ([], False)
-- > "*" -- ([], True)
-- > "foo/#" -- ([Static "foo", Dynamic], False)
-- > "foo/#/*" -- ([Static "foo", Dynamic], True)
mkPieces ::
T.Text -- ^ Path to parse
-> ([D.Piece], Bool) -- ^ list of 'D.Piece's and 'D.hasMulti'
mkPieces "" = ([], False)
mkPieces !t =
if T.last t == '*'
then (prep . T.init $ t, True)
else (prep t, False)
where
prep = map chunk . filter (/="") . T.split (=='/')
{-# INLINE prep #-}
-- | Convert chunk
chunk "#" = D.Dynamic
chunk c = D.Static c
{-# INLINE chunk #-}
{-# INLINE mkPieces #-}
-----------------------------------------------------------------------------
-- Middleware.
-----------------------------------------------------------------------------
-- | Dispatch 'Middleware'.
--
-- > rs :: Dispatch Application
-- > rs = toDispatch . mkRoutes [
-- > Get "foo" fooGetApp
-- > , Post "foo" fooPostApp
-- > , Get "foo//bar" fooDynBarApp
-- >
-- > , Any "any" anyMethodApp
-- > ]
-- >
-- > app :: Application
-- > app = dispatch True rs (error "Not dispatched")
dispatch ::
Bool
-- ^ Squash empty 'pathInfo' chunks. It often appear in the
-- presence of double slashes or \"Ending slash\" in URL.
-> D.Dispatch Application
-- ^ Dispatch function.
-- Use 'D.toDispatch' and route helpers.
-> Application
-- ^ Default (@404@) application.
-> Application
dispatch squash mappings defApp req =
case mappings . needle $ req of
Nothing -> defApp req
(Just app) -> app req
where
needle = (:) <$> decodeUtf8 . requestMethod <*> path squash
path False = pathInfo
path True = filter (/="") . pathInfo
-- | Dispatch 'Middleware' with auto-squash empty path pieces. Equiwalent to
-- > dispatch True
dispatch_ ::
D.Dispatch Application
-- ^ Dispatch function.
-- Use 'D.toDispatch' and route helpers.
-> Application
-- ^ Default (@404@) application.
-> Application
dispatch_ = dispatch True