reroute-0.7.0.0: src/Web/Routing/SafeRouting.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
module Web.Routing.SafeRouting where
#if MIN_VERSION_base(4,11,0)
#elif MIN_VERSION_base(4,9,0)
import Data.Semigroup
#elif MIN_VERSION_base(4,8,0)
import Data.Monoid ((<>))
#else
import Control.Applicative ((<$>))
import Data.Monoid (Monoid (..), (<>))
#endif
import Control.DeepSeq (NFData (..))
import Data.HVect hiding (length, null)
import qualified Data.HVect as HV
import qualified Data.HashMap.Strict as HM
import Data.Maybe
import qualified Data.PolyMap as PM
import qualified Data.Text as T
import Data.Typeable (Typeable)
import Web.HttpApiData
data RouteHandle m a
= forall as. RouteHandle (PathInternal as) (HVectElim as (m a))
newtype HVectElim' x ts = HVectElim' {flipHVectElim :: HVectElim ts x}
type Registry m a = (PathMap (m a), [[T.Text] -> m a])
emptyRegistry :: Registry m a
emptyRegistry = (emptyPathMap, [])
defRoute :: PathInternal xs -> HVectElim' (m a) xs -> Registry m a -> Registry m a
defRoute path action (m, call) =
( insertPathMap (RouteHandle path (flipHVectElim action)) m,
call
)
fallbackRoute :: ([T.Text] -> m a) -> Registry m a -> Registry m a
fallbackRoute routeDef (m, call) = (m, call ++ [routeDef])
matchRoute :: Registry m a -> [T.Text] -> [m a]
matchRoute (m, cAll) pathPieces =
let matches = match m pathPieces
matches' =
if null matches
then matches ++ (map (\f -> f pathPieces) cAll)
else matches
in matches'
data PathInternal (as :: [*]) where
PI_Empty :: PathInternal '[] -- the empty path
PI_StaticCons :: T.Text -> PathInternal as -> PathInternal as -- append a static path piece to path
PI_VarCons :: (FromHttpApiData a, Typeable a) => PathInternal as -> PathInternal (a ': as) -- append a param to path
PI_Wildcard :: PathInternal as -> PathInternal (T.Text ': as) -- append the rest of the route
data PathMap x = PathMap
{ pm_subComponents :: [[T.Text] -> x],
pm_here :: [x],
pm_staticMap :: HM.HashMap T.Text (PathMap x),
pm_polyMap :: PM.PolyMap FromHttpApiData PathMap x,
pm_wildcards :: [T.Text -> x]
}
instance Functor PathMap where
fmap f (PathMap c h s p w) =
PathMap (fmap f <$> c) (f <$> h) (fmap f <$> s) (f <$> p) (fmap f <$> w)
instance NFData x => NFData (PathMap x) where
rnf (PathMap c h s p w) =
rnf c `seq` rnf h `seq` rnf s `seq` PM.rnfHelper rnf p `seq` rnf w
emptyPathMap :: PathMap x
emptyPathMap = PathMap mempty mempty mempty PM.empty mempty
instance Semigroup (PathMap x) where
(PathMap c1 h1 s1 p1 w1) <> (PathMap c2 h2 s2 p2 w2) =
PathMap (c1 <> c2) (h1 <> h2) (HM.unionWith (<>) s1 s2) (PM.unionWith (<>) p1 p2) (w1 <> w2)
instance Monoid (PathMap x) where
mempty = emptyPathMap
mappend = (<>)
updatePathMap ::
(forall y. (ctx -> y) -> PathMap y -> PathMap y) ->
PathInternal ts ->
(HVect ts -> ctx -> x) ->
PathMap x ->
PathMap x
updatePathMap updateFn path action pm@(PathMap c h s p w) =
case path of
PI_Empty -> updateFn (action HNil) pm
PI_StaticCons pathPiece path' ->
let subPathMap = fromMaybe emptyPathMap (HM.lookup pathPiece s)
in PathMap c h (HM.insert pathPiece (updatePathMap updateFn path' action subPathMap) s) p w
PI_VarCons path' ->
let alterFn =
Just . updatePathMap updateFn path' (\vs ctx v -> action (v :&: vs) ctx)
. fromMaybe emptyPathMap
in PathMap c h s (PM.alter alterFn p) w
PI_Wildcard PI_Empty ->
let (PathMap _ (action' : _) _ _ _) = updateFn (\ctx rest -> action (rest :&: HNil) ctx) emptyPathMap
in PathMap c h s p $ action' : w
PI_Wildcard _ -> error "Shouldn't happen"
insertPathMap' :: PathInternal ts -> (HVect ts -> x) -> PathMap x -> PathMap x
insertPathMap' path action =
let updateHeres y (PathMap c h s p w) = PathMap c (y () : h) s p w
in updatePathMap updateHeres path (const <$> action)
singleton :: PathInternal ts -> HVectElim ts x -> PathMap x
singleton path action = insertPathMap' path (HV.uncurry action) mempty
insertPathMap :: RouteHandle m a -> PathMap (m a) -> PathMap (m a)
insertPathMap (RouteHandle path action) = insertPathMap' path (HV.uncurry action)
insertSubComponent' :: PathInternal ts -> (HVect ts -> [T.Text] -> x) -> PathMap x -> PathMap x
insertSubComponent' path subComponent =
let updateSubComponents y (PathMap c h s p w) = PathMap (y : c) h s p w
in updatePathMap updateSubComponents path subComponent
insertSubComponent :: Functor m => RouteHandle m ([T.Text] -> a) -> PathMap (m a) -> PathMap (m a)
insertSubComponent (RouteHandle path comp) =
insertSubComponent' path (fmap (\m ps -> fmap ($ ps) m) (HV.uncurry comp))
match :: PathMap x -> [T.Text] -> [x]
match (PathMap c h s p w) pieces =
map ($ pieces) c
++ case pieces of
[] -> h ++ fmap ($ "") w
(pp : pps) ->
let staticMatches = maybeToList (HM.lookup pp s) >>= flip match pps
varMatches =
PM.lookupConcat
(either (const Nothing) Just $ parseUrlPiece pp)
(\piece pathMap' -> fmap ($ piece) (match pathMap' pps))
p
routeRest = combineRoutePieces pieces
wildcardMatches = fmap ($ routeRest) w
in staticMatches ++ varMatches ++ wildcardMatches
(</!>) :: PathInternal as -> PathInternal bs -> PathInternal (Append as bs)
(</!>) PI_Empty xs = xs
(</!>) (PI_StaticCons pathPiece xs) ys = PI_StaticCons pathPiece (xs </!> ys)
(</!>) (PI_VarCons xs) ys = PI_VarCons (xs </!> ys)
(</!>) (PI_Wildcard _) _ = error "Shouldn't happen"
combineRoutePieces :: [T.Text] -> T.Text
combineRoutePieces = T.intercalate "/"
parse :: PathInternal as -> [T.Text] -> Maybe (HVect as)
parse PI_Empty [] = Just HNil
parse _ [] = Nothing
parse path pathComps@(pathComp : xs) =
case path of
PI_Empty -> Nothing
PI_StaticCons pathPiece pathXs ->
if pathPiece == pathComp
then parse pathXs xs
else Nothing
PI_VarCons pathXs ->
case parseUrlPiece pathComp of
Left _ -> Nothing
Right val ->
let finish = parse pathXs xs
in fmap (\parsedXs -> val :&: parsedXs) finish
PI_Wildcard PI_Empty ->
Just $ combineRoutePieces pathComps :&: HNil
PI_Wildcard _ ->
error "Shouldn't happen"