{-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-} ----------------------------------------------------------------------------- -- | -- Module : Webcrank.Dispatch -- Copyright : (C) 2015 Richard Wallace -- License : BSD-style (see the file LICENSE) -- Maintainer : Richard Wallace -- Stability : provisional ----------------------------------------------------------------------------- module Webcrank.Dispatch ( -- * Paths -- ** Building Paths root , () , param , RR.Path -- ** Rendering Paths , renderPath , params , HBuild'(..) -- * Dispatching , (==>) , dispatch , Dispatcher ) where import Control.Monad.Identity import qualified Data.HashMap.Strict as HM import Data.HVect import Data.Maybe import Data.Monoid import Data.Text (Text) import Data.Typeable (Typeable) import Web.PathPieces import Web.Routing.AbstractRouter import qualified Web.Routing.SafeRouting as RR -- | The simplest @'Path'@ is the @root@ path, which is equivalent to @/@. root :: RR.Path '[] root = RR.root -- | Other routes can be built with @@: -- -- @ -- docsPath = "package" \<\/> "webcrank-dispatch-0.1" \<\/> "docs" -- @ () :: RR.Path as -> RR.Path bs -> RR.Path (Append as bs) () = (RR.) -- | Paths can contain parameters. To create a parameterized path, use -- @param@ as a path component: -- -- @ -- docsPath :: Path '[String] -- docsPath = "package" \<\/> param \<\/> "docs" -- @ -- -- Paths can contain as many parameters of varying types as needed: -- -- @ -- wat :: Path '[String, Int, Bool, Int, String] -- wat :: "this" \<\/> param \<\/> param \<\/> "crazyness" \<\/> param \<\/> "ends" \<\/> param \<\/> param -- @ -- -- Path parameters can be of any type that have instances for @'Typeable'@ -- and @'PathPiece'@. param :: (Typeable a, PathPiece a) => RR.Path (a ': '[]) param = RR.var -- | @Path@s can be rendered using @'renderPath'@ and -- @'params'@. -- -- >>> renderPath root params -- ["/"] -- -- >>> renderPath docsPath $ params "webcrank-dispatch-0.1" -- ["package", "webcrank-dispatch-0.1", "docs"] -- -- >>> renderPath wat $ params "down is up" 42 False 7 "up is down" -- ["this", "down is up", "42", "crazyness", "False", "ends", "7", "up is down"] -- -- Note in the last example that no encoding is done by @renderPath@. renderPath :: RR.Path l -> HVect l -> [Text] renderPath = RR.renderRoute' params :: (HBuild' '[] r) => r params = hBuild' HNil class HBuild' l r where hBuild' :: HVect l -> r instance (l' ~ ReverseLoop l '[]) => HBuild' l (HVect l') where hBuild' l = hVectReverse l instance HBuild' (a ': l) r => HBuild' l (a -> r) where hBuild' l x = hBuild' (HCons x l) -- | An elementary @'Dispatcher'@ can be built using @'==>'@. -- -- @disp = root ==> \"Dispatched\"@ -- -- @Dispatcher@s form a @'Monoid'@, so more interesting dispatchers can -- be built with @'<>'@ or @'mconcat'@. -- -- @ -- disp = mconcat -- [ root ==> "Welcome!" -- , "echo" param ==> id -- ] -- @ (==>) :: RR.Path as -> HVectElim as a -> Dispatcher a (==>) p r = Dispatcher $ hookRoute () (SafeRouterPath p) (RR.HVectElim' r) infixr 8 ==> -- | Dispatching requests is done with @'dispatch'@. It turns a -- @Dispatcher@ into a function from a list of decoded path components -- to a possible handler. -- -- >>> dispatch (root ==> "Welcome!") [""] -- Just "Welcome!" -- -- >>> dispatch (root ==> "Welcome!") ["echo", "Goodbye!"] -- Nothing -- -- >>> dispatch (root ==> "Welcome!" <> "echo" param ==> id) ["echo", "Goodbye!"] -- Just "Goodbye!" dispatch :: Dispatcher a -> [Text] -> Maybe a dispatch (Dispatcher r) = case runIdentity $ runRegistry SafeRouter r of (_, f, _) -> fmap snd . listToMaybe . f () newtype Dispatcher a = Dispatcher (RegistryT (SafeRouter a) () () Identity ()) instance Monoid (Dispatcher a) where mempty = Dispatcher $ return () mappend (Dispatcher x) (Dispatcher y) = Dispatcher $ x >> y data SafeRouter a = SafeRouter instance AbstractRouter (SafeRouter a) where newtype Registry (SafeRouter a) = SafeRouterReg (RR.PathMap a, [[Text] -> a]) newtype RoutePath (SafeRouter a) xs = SafeRouterPath (RR.Path xs) type RouteAction (SafeRouter a) = RR.HVectElim' a type RouteAppliedAction (SafeRouter a) = a subcompCombine (SafeRouterPath p1) (SafeRouterPath p2) = SafeRouterPath $ p1 p2 emptyRegistry = SafeRouterReg (RR.emptyPathMap, []) rootPath = SafeRouterPath RR.Empty defRoute (SafeRouterPath path) action (SafeRouterReg (a, cAll)) = SafeRouterReg ( RR.insertPathMap' path (hVectUncurry $ RR.flipHVectElim action) a , cAll ) fallbackRoute routeDef (SafeRouterReg (a, cAll)) = SafeRouterReg (a, cAll <> [routeDef]) matchRoute (SafeRouterReg (a, cAll)) pathPieces = let matches = RR.match a pathPieces matches' = if null matches then matches <> fmap (\f -> f pathPieces) cAll else matches in zip (replicate (length matches') HM.empty) matches'