{-# 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 as HV 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 = HV.reverse l instance HBuild' (a ': l) r => HBuild' l (a -> r) where hBuild' l x = hBuild' (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 (HV.uncurry $ 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 Prelude.null matches then matches <> fmap (\f -> f pathPieces) cAll else matches in zip (replicate (Prelude.length matches') HM.empty) matches'