reroute 0.1.0.0 → 0.2.0.0
raw patch · 8 files changed
+483/−196 lines, 8 filesdep +deepseqdep +graph-coredep −HList
Dependencies added: deepseq, graph-core
Dependencies removed: HList
Files
- reroute.cabal +9/−6
- src/Data/HVect.hs +84/−0
- src/Data/PolyMap.hs +194/−0
- src/Web/Routing/AbstractRouter.hs +9/−7
- src/Web/Routing/SafeRouting.hs +63/−64
- src/Web/Routing/Specs/SafeRoutingSpec.hs +27/−5
- src/Web/Routing/Specs/TextRoutingSpec.hs +2/−31
- src/Web/Routing/TextRouting.hs +95/−83
reroute.cabal view
@@ -1,5 +1,5 @@ name: reroute-version: 0.1.0.0+version: 0.2.0.0 synopsis: abstract implementation of typed and untyped web routing description: abstraction over how urls with/without parameters are mapped to their corresponding handlers homepage: http://github.com/agrafix/reroute@@ -13,18 +13,20 @@ cabal-version: >=1.10 library- exposed-modules: Web.Routing.AbstractRouter, Web.Routing.SafeRouting, Web.Routing.TextRouting+ exposed-modules: Data.HVect, Web.Routing.AbstractRouter, Web.Routing.SafeRouting, Web.Routing.TextRouting+ other-modules: Data.PolyMap build-depends: base >=4.6 && <4.8, transformers >=0.3 && <0.5, text >= 0.11.3.1 && <1.3, unordered-containers ==0.2.*,- HList >=0.3, regex-compat ==0.95.*, hashable >=1.2 && <1.3, mtl >=2.1 && <2.3, path-pieces >=0.1 && <0.2, hspec2 >=0.4 && <0.5,- vector >=0.10 && <0.11+ vector >=0.10 && <0.11,+ deepseq >= 1.1.0.2 && < 1.4,+ graph-core >=0.2 && <0.3 hs-source-dirs: src default-language: Haskell2010 @@ -38,13 +40,14 @@ transformers >=0.3 && <0.5, text >= 0.11.3.1 && <1.3, unordered-containers ==0.2.*,- HList >=0.3, regex-compat ==0.95.*, hashable >=1.2 && <1.3, mtl >=2.1 && <2.3, path-pieces >=0.1 && <0.2, hspec2 >=0.4 && <0.5,- vector >=0.10 && <0.11+ vector >=0.10 && <0.11,+ deepseq >= 1.1.0.2 && < 1.4,+ graph-core >=0.2 && <0.3 default-language: Haskell2010 ghc-options: -Wall -fno-warn-orphans
+ src/Data/HVect.hs view
@@ -0,0 +1,84 @@+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE UndecidableInstances #-} -- for ReverseLoop type family+{-# LANGUAGE Rank2Types #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE KindSignatures #-}++module Data.HVect+ ( HVect (..)+ , HVectElim+ , Append, hVectAppend+ , ReverseLoop, Reverse, hVectReverse+ , hVectUncurry+ , Rep (..), HasRep (..)+ , hVectCurryExpl, hVectCurry+ , packExpl, pack+ ) where++data HVect (ts :: [*]) where+ HNil :: HVect '[]+ HCons :: t -> HVect ts -> HVect (t ': ts)++-- todo: use a closed type family once GHC 7.6 compatibility is dropped+type family HVectElim (ts :: [*]) (a :: *) :: *+type instance HVectElim '[] a = a+type instance HVectElim (t ': ts) a = t -> HVectElim ts a++-- todo: use a closed type family once GHC 7.6 compatibility is dropped+type family Append (as :: [*]) (bs :: [*]) :: [*]+type instance Append '[] bs = bs+type instance Append (a ': as) bs = a ': (Append as bs)++hVectAppend :: HVect as -> HVect bs -> HVect (Append as bs)+hVectAppend HNil bs = bs+hVectAppend (HCons a as) bs = HCons a (hVectAppend as bs)++type family ReverseLoop (as :: [*]) (bs :: [*]) :: [*]+type instance ReverseLoop '[] bs = bs+type instance ReverseLoop (a ': as) bs = ReverseLoop as (a ': bs)++type Reverse as = ReverseLoop as '[]++hVectReverse :: HVect as -> HVect (Reverse as)+hVectReverse vs = go vs HNil+ where+ go :: HVect as -> HVect bs -> HVect (ReverseLoop as bs)+ go HNil bs = bs+ go (HCons a as) bs = go as (HCons a bs)++hVectUncurry :: HVectElim ts a -> HVect ts -> a+hVectUncurry f HNil = f+hVectUncurry f (HCons x xs) = hVectUncurry (f x) xs++data Rep (ts :: [*]) where+ RNil :: Rep '[]+ RCons :: Rep ts -> Rep (t ': ts)++class HasRep (ts :: [*]) where+ hasRep :: Rep ts++instance HasRep '[] where+ hasRep = RNil++instance HasRep ts => HasRep (t ': ts) where+ hasRep = RCons hasRep++hVectCurryExpl :: Rep ts -> (HVect ts -> a) -> HVectElim ts a+hVectCurryExpl RNil f = f HNil+hVectCurryExpl (RCons r) f = \x -> hVectCurryExpl r (f . HCons x)++hVectCurry :: HasRep ts => (HVect ts -> a) -> HVectElim ts a+hVectCurry = hVectCurryExpl hasRep++buildElim :: Rep ts -> (HVect ts -> HVect ss) -> HVectElim ts (HVect ss)+buildElim RNil f = f HNil+buildElim (RCons r) f = \x -> buildElim r (f . HCons x)++packExpl :: Rep ts -> (forall a. HVectElim ts a -> a) -> HVect ts+packExpl rep f = f (buildElim rep id)++pack :: HasRep ts => (forall a. HVectElim ts a -> a) -> HVect ts+pack = packExpl hasRep
+ src/Data/PolyMap.hs view
@@ -0,0 +1,194 @@+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE Rank2Types #-}+{-# LANGUAGE GADTs #-}++module Data.PolyMap+ ( PolyMap, empty, rnfHelper+ , lookup, lookupApply, lookupApplyAll, lookupConcat+ , alter, updateAll, insertWith+ , unionWith, union+ , zipWith', zipWith, zip, ap+ ) where++import Prelude hiding (lookup, zipWith, zip)+import Data.Typeable+import Control.Applicative (Applicative (..), Alternative ((<|>)), liftA2)+import Data.Monoid (Monoid (..))+import GHC.Exts (Constraint)++data PolyMap (c :: * -> Constraint) (f :: * -> *) (a :: *) where+ PMNil :: PolyMap c f a+ PMCons :: (Typeable p, c p) => f (p -> a) -> PolyMap c f a -> PolyMap c f a++instance Functor f => Functor (PolyMap c f) where+ fmap _ PMNil = PMNil+ fmap f (PMCons v pm) = PMCons (fmap ((.) f) v) (fmap f pm)++instance Alternative f => Monoid (PolyMap c f a) where+ mempty = empty+ mappend = union++empty :: PolyMap c f a+empty = PMNil++rnfHelper :: (forall p. c p => f (p -> a) -> ()) -> PolyMap c f a -> ()+rnfHelper _ PMNil = ()+rnfHelper h (PMCons v pm) = h v `seq` rnfHelper h pm++lookup ::+ Typeable p+ => PolyMap c f a+ -> Maybe (f (p -> a))+lookup PMNil = Nothing+lookup (PMCons w polyMap') =+ case gcast1 w of+ Just w' -> Just w'+ Nothing -> lookup polyMap'++lookupApply ::+ (Typeable p, Functor f)+ => p+ -> PolyMap c f a+ -> Maybe (f a)+lookupApply _ PMNil = Nothing+lookupApply p (PMCons w polyMap') =+ case gcast1 w of+ Just w' -> Just (fmap ($ p) w')+ Nothing -> lookupApply p polyMap'++lookupApplyAll ::+ Functor f+ => (forall p. c p => Maybe p)+ -> PolyMap c f a+ -> [f a]+lookupApplyAll maybeGet polyMap =+ case polyMap of+ PMNil -> []+ PMCons w polyMap' ->+ let rest = lookupApplyAll maybeGet polyMap'+ in case maybeGet of+ Nothing -> rest+ Just p -> (fmap ($ p) w) : rest++lookupConcat ::+ (Monoid m, Functor f)+ => (forall p. c p => Maybe p)+ -> (forall p. c p => p -> f (p -> a) -> m)+ -> PolyMap c f a+ -> m+lookupConcat maybeGet comp polyMap =+ case polyMap of+ PMNil -> mempty+ PMCons w polyMap' ->+ let rest = lookupConcat maybeGet comp polyMap'+ in case maybeGet of+ Nothing -> rest+ Just p -> comp p w `mappend` rest+++maybeInsertHere ::+ (c p, Typeable p)+ => Maybe (f (p -> a))+ -> PolyMap c f a+ -> PolyMap c f a+maybeInsertHere = maybe id PMCons++alter ::+ (Typeable p, c p)+ => (Maybe (f (p -> a)) -> Maybe (f (p -> a)))+ -> PolyMap c f a+ -> PolyMap c f a+alter (g :: Maybe (f (p -> a)) -> Maybe (f (p -> a))) polyMap =+ case polyMap of+ PMNil -> insertHere PMNil+ PMCons (w :: f (q -> a)) polyMap' ->+ case gcast1 w of+ Just w' ->+ case g (Just w') of+ Just w'' -> PMCons w'' polyMap'+ Nothing -> polyMap'+ Nothing ->+ if typeOf (undefined :: p) < typeOf (undefined :: q)+ then insertHere polyMap+ else PMCons w (alter g polyMap')+ where insertHere = maybe id PMCons (g Nothing)++updateAll ::+ (forall p. c p => f (p -> a) -> g (p -> b))+ -> PolyMap c f a+ -> PolyMap c g b+updateAll _ PMNil = PMNil+updateAll f (PMCons v pm) = PMCons (f v) (updateAll f pm)++insertWith ::+ (Typeable p, c p)+ => (f (p -> a) -> f (p -> a) -> f (p -> a))+ -> f (p -> a)+ -> PolyMap c f a+ -> PolyMap c f a+insertWith combine val = alter (Just . maybe val (combine val))++unionWith ::+ (forall p. c p => f (p -> a) -> f (p -> a) -> f (p -> a))+ -> PolyMap c f a+ -> PolyMap c f a+ -> PolyMap c f a+unionWith _ PMNil pm2 = pm2+unionWith _ pm1 PMNil = pm1+unionWith combine pm1@(PMCons (v :: f (p -> a)) pm1')+ pm2@(PMCons (w :: f (q -> a)) pm2') =+ case gcast1 v of+ Just v' -> PMCons (combine v' w) (unionWith combine pm1' pm2')+ Nothing ->+ if typeOf (undefined :: p) < typeOf (undefined :: q)+ then PMCons v (unionWith combine pm1' pm2)+ else PMCons w (unionWith combine pm1 pm2')++union ::+ Alternative f+ => PolyMap c f a+ -> PolyMap c f a+ -> PolyMap c f a+union = unionWith (<|>)++zipWith' :: + (forall p. c p => Maybe (f (p -> a)) -> Maybe (f (p -> b)) -> Maybe (f (p -> d)))+ -> PolyMap c f a+ -> PolyMap c f b+ -> PolyMap c f d+zipWith' f = go+ where+ go PMNil PMNil = PMNil+ go PMNil (PMCons w pm2') = maybeInsertHere (f Nothing (Just w)) (go PMNil pm2')+ go (PMCons v pm1') PMNil = maybeInsertHere (f (Just v) Nothing) (go pm1' PMNil)+ go pm1@(PMCons (v :: f (p -> a)) pm1') pm2@(PMCons (w :: f (q -> b)) pm2') =+ case gcast1 v of+ Just v' -> maybeInsertHere (f (Just v') (Just w)) (go pm1 pm2)+ Nothing ->+ if typeOf (undefined :: p) < typeOf (undefined :: q)+ then maybeInsertHere (f (Just v) Nothing) (go pm1' pm2)+ else maybeInsertHere (f Nothing (Just w)) (go pm1 pm2')++zipWith ::+ Applicative f+ => (a -> b -> d)+ -> PolyMap c f a+ -> PolyMap c f b+ -> PolyMap c f d+zipWith f = zipWith' $ liftA2 $ liftA2 $ \a b p -> f (a p) (b p)++zip ::+ Applicative f+ => PolyMap c f a+ -> PolyMap c f b+ -> PolyMap c f (a, b)+zip = zipWith (,)++ap ::+ Applicative f+ => PolyMap c f (a -> b)+ -> PolyMap c f a+ -> PolyMap c f b+ap = zipWith ($)
src/Web/Routing/AbstractRouter.hs view
@@ -10,6 +10,7 @@ import Data.Maybe import qualified Data.HashMap.Strict as HM import qualified Data.Text as T+import Control.DeepSeq (NFData (..)) class AbstractRouter r where data Registry r :: *@@ -26,7 +27,7 @@ newtype CaptureVar = CaptureVar { unCaptureVar :: T.Text }- deriving (Show, Eq, Hashable)+ deriving (Show, Eq, Hashable, NFData) newtype RegistryT r middleware reqTypes (m :: * -> *) a = RegistryT { runRegistryT :: RWST (RoutePath r '[]) [middleware] (RegistryState r reqTypes) m a }@@ -48,12 +49,13 @@ -> RouteAction r as -> RegistryT r middleware reqTypes m () hookRoute reqType path action =- modify $ \rs ->- rs { rs_registry =- let reg = fromMaybe emptyRegistry (HM.lookup reqType (rs_registry rs))- reg' = defRoute path action reg- in HM.insert reqType reg' (rs_registry rs)- }+ do basePath <- ask+ modify $ \rs ->+ rs { rs_registry =+ let reg = fromMaybe emptyRegistry (HM.lookup reqType (rs_registry rs))+ reg' = defRoute (basePath `subcompCombine` path) action reg+ in HM.insert reqType reg' (rs_registry rs)+ } middleware :: Monad m => middleware
src/Web/Routing/SafeRouting.hs view
@@ -1,32 +1,38 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-}+ module Web.Routing.SafeRouting where +import qualified Data.PolyMap as PM+import Data.HVect import Web.Routing.AbstractRouter -import Data.HList import Data.Maybe-import Data.Monoid+import Data.List (foldl')+import Data.Monoid (Monoid (..))+import Control.Applicative (Applicative (..), Alternative (..)) import Data.String+import Data.Typeable (Typeable)+import Control.DeepSeq (NFData (..)) import Web.PathPieces import qualified Data.HashMap.Strict as HM import qualified Data.Text as T data RouteHandle m a- = forall as. RouteHandle (Path as) (HListElim as (m a))+ = forall as. RouteHandle (Path as) (HVectElim as (m a)) -newtype HListElim' x ts = HListElim' { flipHListElim :: HListElim ts x }+newtype HVectElim' x ts = HVectElim' { flipHVectElim :: HVectElim ts x } data SafeRouter (m :: * -> *) a = SafeRouter instance AbstractRouter (SafeRouter m a) where newtype Registry (SafeRouter m a) = SafeRouterReg (PathMap (m a)) newtype RoutePath (SafeRouter m a) xs = SafeRouterPath (Path xs)- type RouteAction (SafeRouter m a) = HListElim' (m a)+ type RouteAction (SafeRouter m a) = HVectElim' (m a) type RouteAppliedAction (SafeRouter m a) = m a subcompCombine (SafeRouterPath p1) (SafeRouterPath p2) = SafeRouterPath $@@ -35,81 +41,72 @@ rootPath = SafeRouterPath Empty defRoute (SafeRouterPath path) action (SafeRouterReg m) = SafeRouterReg $- insertPathMap (RouteHandle path (flipHListElim action)) m+ insertPathMap (RouteHandle path (flipHVectElim action)) m matchRoute (SafeRouterReg m) pathPieces = let matches = match m pathPieces in zip (replicate (length matches) HM.empty) matches -type family HListElim (ts :: [*]) (a :: *) :: *-type instance HListElim '[] a = a-type instance HListElim (t ': ts) a = t -> HListElim ts a -hListUncurry :: HListElim ts a -> HList ts -> a-hListUncurry f HNil = f-hListUncurry f (HCons x xs) = hListUncurry (f x) xs- data Path (as :: [*]) where Empty :: Path '[] -- the empty path StaticCons :: T.Text -> Path as -> Path as -- append a static path piece to path VarCons :: (PathPiece a, Typeable a) => Path as -> Path (a ': as) -- append a param to path -data PolyMap x where- PMNil :: PolyMap x- PMCons :: (Typeable a, PathPiece a) => PathMap (a -> x) -> PolyMap x -> PolyMap x--data Id a = Id { getId :: a }--castDefType :: (Typeable a, Typeable b) => (a -> c) -> Maybe (b -> c)-castDefType x = fmap getId (gcast1 (Id x))--insertPolyMap ::- (Typeable a, PathPiece a)- => Path ts- -> (a -> HList ts -> x)- -> PolyMap x- -> PolyMap x-insertPolyMap path (action :: a -> HList ts -> x) polyMap =- case polyMap of- PMNil -> PMCons (insertPathMap' path (flip action) emptyPathMap) PMNil- PMCons (pathMap :: PathMap (b -> x)) polyMap' ->- case castDefType action of- Just action' ->- PMCons (insertPathMap' path (flip action') pathMap) polyMap'- Nothing ->- PMCons pathMap (insertPolyMap path action polyMap')+data PathMap x =+ PathMap+ { pm_here :: [x]+ , pm_staticMap :: HM.HashMap T.Text (PathMap x)+ , pm_polyMap :: PM.PolyMap PathPiece PathMap x+ } -lookupPolyMap :: T.Text -> [T.Text] -> PolyMap x -> [x]-lookupPolyMap _ _ PMNil = []-lookupPolyMap pp pps (PMCons pathMap polyMap') =- (maybeToList (fromPathPiece pp) >>= \val -> fmap ($ val) (match pathMap pps))- ++ lookupPolyMap pp pps polyMap'+instance Functor PathMap where+ fmap f (PathMap h s p) = PathMap (fmap f h) (fmap (fmap f) s) (fmap f p) +instance Applicative PathMap where+ pure x = PathMap [x] mempty PM.empty+ PathMap h s p <*> y =+ let start = PathMap mempty (fmap (<*> y) s) (PM.updateAll (\a -> fmap flip a <*> y) p)+ in foldl' (\pm f -> pm `mappend` fmap f y) start h -emptyPolyMap :: PolyMap x-emptyPolyMap = PMNil+instance Alternative PathMap where+ empty = emptyPathMap+ (<|>) = mappend -data PathMap x = PathMap [x] (HM.HashMap T.Text (PathMap x)) (PolyMap x)+instance NFData x => NFData (PathMap x) where+ rnf (PathMap h s p) = rnf h `seq` rnf s `seq` PM.rnfHelper rnf p emptyPathMap :: PathMap x-emptyPathMap = PathMap mempty mempty emptyPolyMap+emptyPathMap = PathMap mempty mempty PM.empty -insertPathMap' :: Path ts -> (HList ts -> x) -> PathMap x -> PathMap x-insertPathMap' path action (PathMap as m pm) =+instance Monoid (PathMap x) where+ mempty = emptyPathMap+ mappend (PathMap h1 s1 p1) (PathMap h2 s2 p2) =+ PathMap (h1 `mappend` h2) (HM.unionWith mappend s1 s2) (PM.unionWith mappend p1 p2)++insertPathMap' :: Path ts -> (HVect ts -> x) -> PathMap x -> PathMap x+insertPathMap' path action (PathMap h s p) = case path of- Empty -> PathMap (action HNil : as) m pm- StaticCons pathPiece xs ->- let subPathMap = fromMaybe emptyPathMap (HM.lookup pathPiece m)- in PathMap as (HM.insert pathPiece (insertPathMap' xs action subPathMap) m) pm- VarCons xs -> PathMap as m (insertPolyMap xs (\v vs -> action (HCons v vs)) pm)+ Empty -> PathMap (action HNil : h) s p+ StaticCons pathPiece path' ->+ let subPathMap = fromMaybe emptyPathMap (HM.lookup pathPiece s)+ in PathMap h (HM.insert pathPiece (insertPathMap' path' action subPathMap) s) p+ VarCons path' ->+ let alterFn = Just . insertPathMap' path' (\vs v -> action (HCons v vs))+ . fromMaybe emptyPathMap+ in PathMap h s (PM.alter alterFn p) +singleton :: Path ts -> HVectElim ts x -> PathMap x+singleton path action = insertPathMap' path (hVectUncurry action) mempty+ insertPathMap :: RouteHandle m a -> PathMap (m a) -> PathMap (m a)-insertPathMap (RouteHandle path action) = insertPathMap' path (hListUncurry action)+insertPathMap (RouteHandle path action) = insertPathMap' path (hVectUncurry action) match :: PathMap x -> [T.Text] -> [x]-match (PathMap as _ _) [] = as-match (PathMap _ m pm) (pp:pps) =- let staticMatches = maybeToList (HM.lookup pp m) >>= flip match pps- varMatches = lookupPolyMap pp pps pm+match (PathMap h _ _) [] = h+match (PathMap _ s p) (pp:pps) =+ let staticMatches = maybeToList (HM.lookup pp s) >>= flip match pps+ varMatches = PM.lookupConcat (fromPathPiece pp)+ (\piece pathMap' -> fmap ($ piece) (match pathMap' pps)) p in staticMatches ++ varMatches -- | A route parameter@@ -120,7 +117,9 @@ -- | A static route piece static :: String -> Path '[]-static s = StaticCons (T.pack s) Empty+static s =+ let pieces = filter (not . T.null) $ T.splitOn "/" $ T.pack s+ in foldr StaticCons Empty pieces instance (a ~ '[]) => IsString (Path a) where fromString = static@@ -129,16 +128,16 @@ root :: Path '[] root = Empty -(</>) :: Path as -> Path bs -> Path (HAppendList as bs)+(</>) :: Path as -> Path bs -> Path (Append as bs) (</>) Empty xs = xs (</>) (StaticCons pathPiece xs) ys = (StaticCons pathPiece (xs </> ys)) (</>) (VarCons xs) ys = (VarCons (xs </> ys)) -renderRoute :: Path as -> HList as -> T.Text+renderRoute :: Path as -> HVect as -> T.Text renderRoute p h = T.intercalate "/" $ renderRoute' p h -renderRoute' :: Path as -> HList as -> [T.Text]+renderRoute' :: Path as -> HVect as -> [T.Text] renderRoute' Empty _ = [] renderRoute' (StaticCons pathPiece pathXs) paramXs = ( pathPiece : renderRoute' pathXs paramXs )@@ -147,7 +146,7 @@ renderRoute' _ _ = error "This will never happen." -parse :: Path as -> [T.Text] -> Maybe (HList as)+parse :: Path as -> [T.Text] -> Maybe (HVect as) parse Empty [] = Just HNil parse _ [] = Nothing parse path (pathComp : xs) =
src/Web/Routing/Specs/SafeRoutingSpec.hs view
@@ -8,10 +8,13 @@ module Web.Routing.Specs.SafeRoutingSpec where import Test.Hspec+import Data.HVect import Control.Monad.Identity import Web.Routing.SafeRouting import Web.Routing.AbstractRouter+import Data.Monoid (mconcat)+import Control.Applicative (Applicative (..)) import qualified Data.Text as T data ReturnVar@@ -21,14 +24,14 @@ | ListVar [ReturnVar] deriving (Show, Eq, Read) -defR :: (Monad m, HListElim ts (m ReturnVar) ~ HListElim ts x) => Path ts -> HListElim ts x -> RegistryT (SafeRouter m ReturnVar) middleware Bool m ()-defR path action = hookRoute True (SafeRouterPath path) (HListElim' action)+defR :: (Monad m, HVectElim ts (m ReturnVar) ~ HVectElim ts x) => Path ts -> HVectElim ts x -> RegistryT (SafeRouter m ReturnVar) middleware Bool m ()+defR path action = hookRoute True (SafeRouterPath path) (HVectElim' action) spec :: Spec spec = describe "SafeRouting Spec" $- do it "shoud match known routes" $+ do it "should match known routes" $ do checkRoute "" [StrVar "root"] checkRoute "/bar" [StrVar "bar"] it "shoudn't match unknown routes" $@@ -45,10 +48,29 @@ do checkRoute "/bar/5" [IntVar 5, StrVar "5"] checkRoute "/bar/bingo" [StrVar "bar/bingo", StrVar "bingo"] checkRoute "/entry/1/audit" [IntVar 1,ListVar [IntVar 1,StrVar "audit"]]+ it "should provide an Applicative interface" $+ do let numbers =+ mconcat+ [ singleton var id+ , singleton ("forty" </> "two") (42 :: Int)+ ]+ operators =+ mconcat+ [ singleton "plus" ((+) :: Int -> Int -> Int)+ , singleton "mult" (*)+ ]+ routes = operators <*> numbers <*> numbers+ check path val = match routes (pieces path) `shouldBe` [val]+ check "/plus/forty/two/forty/two" (42+42)+ check "/mult/forty/two/3" (42*3)+ check "/plus/5/89" 94 where+ pieces :: T.Text -> [T.Text]+ pieces = filter (not . T.null) . T.splitOn "/"+ checkRoute :: T.Text -> [ReturnVar] -> Expectation checkRoute r x =- let matches = handleFun (filter (not . T.null) $ T.splitOn "/" r)+ let matches = handleFun (pieces r) in (map (runIdentity . snd) matches) `shouldBe` x handleFun :: [T.Text] -> [(ParamMap, Identity ReturnVar)]@@ -67,7 +89,7 @@ return (ListVar [IntVar i, IntVar i2]) defR ("entry" </> var </> var) $ \i st -> return (ListVar [IntVar i, StrVar st])- defR ("entry" </> "bytags" </> var </> var) $ \i st ->+ defR ("entry/bytags" </> var </> var) $ \i st -> return (ListVar [IntVar i, StrVar st]) defR ("entry" </> var </> "rel" </> var) $ \i i2 -> return (ListVar [IntVar i, IntVar i2])
src/Web/Routing/Specs/TextRoutingSpec.hs view
@@ -5,7 +5,6 @@ import Web.Routing.TextRouting import qualified Web.Routing.AbstractRouter as R-import qualified Data.Vector as V import qualified Data.HashMap.Strict as HM spec :: Spec@@ -13,7 +12,6 @@ do matchNodeDesc matchRouteDesc parseRouteNodeDesc- addToRoutingTreeDesc matchNodeDesc :: Spec matchNodeDesc =@@ -52,8 +50,8 @@ ((oneMatch emptyParamMap [5]) ++ oneMatch (vMap [("baz", "bingo")]) [3]) multiMatch' =- ((oneMatch (vMap [("since", "audit"), ("cid", "1")]) [6])- ++ (oneMatch (vMap [("eid", "1")]) [8]))+ ((oneMatch (vMap [("eid", "1")]) [8])+ ++ (oneMatch (vMap [("since", "audit"), ("cid", "1")]) [6])) noMatches = [] oneMatch pm m = [(pm, m)] routingTree =@@ -81,30 +79,3 @@ parseRouteNode ":bar" `shouldBe` RouteNodeCapture (R.CaptureVar "bar") it "parses regex capture variables" $ parseRouteNode "{bar:^[0-9]$}" `shouldBe` RouteNodeRegex (R.CaptureVar "bar") (buildRegex "^[0-9]$")--addToRoutingTreeDesc :: Spec-addToRoutingTreeDesc =- describe "addToRoutingTree" $- do it "adds the root node correctly" $- addToRoutingTree "/" [True] emptyT `shouldBe` baseRoute- it "adds a new branch correctly" $- addToRoutingTree "/foo/:bar" [True] emptyT `shouldBe` fooBar []- it "add a new subbranch correctly" $- addToRoutingTree "/foo/:bar/baz" [True] (fooBar []) `shouldBe` fooBar baz- where- emptyT = emptyRoutingTree- baseRoute = RoutingTree { rt_node = RouteData{rd_node = RouteNodeRoot, rd_data = Just [True]}, rt_children = V.empty}- baz = [ RoutingTree { rt_node = RouteData { rd_node = RouteNodeText "baz", rd_data = Just [True] },rt_children = V.empty }]- fooBar xs =- RoutingTree- { rt_node =- RouteData {rd_node = RouteNodeRoot, rd_data = Nothing }- , rt_children =- V.fromList- [ RoutingTree { rt_node = RouteData{rd_node = RouteNodeText "foo", rd_data = Nothing}- , rt_children =- V.fromList- [ RoutingTree { rt_node = RouteData { rd_node = RouteNodeCapture (R.CaptureVar "bar")- , rd_data = Just [True]- }- , rt_children = V.fromList xs}]}]}
src/Web/Routing/TextRouting.hs view
@@ -7,11 +7,13 @@ import Web.Routing.AbstractRouter -import Data.Maybe import Data.String+import Control.DeepSeq (NFData (..))+import qualified Data.Graph as G import qualified Data.HashMap.Strict as HM import qualified Data.Text as T import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU import qualified Data.Vector.Mutable as VM import qualified Text.Regex as Regex @@ -76,6 +78,9 @@ instance Show RegexWrapper where show (RegexWrapper _ x) = show x +instance NFData RegexWrapper where+ rnf (RegexWrapper _ t) = rnf t+ data RouteNode = RouteNodeRegex !CaptureVar !RegexWrapper | RouteNodeCapture !CaptureVar@@ -83,62 +88,111 @@ | RouteNodeRoot deriving (Show, Eq) +instance NFData RouteNode where+ rnf (RouteNodeRegex v w) = rnf v `seq` rnf w+ rnf (RouteNodeCapture v) = rnf v+ rnf (RouteNodeText t) = rnf t+ rnf RouteNodeRoot = ()+ data RouteData a = RouteData { rd_node :: !RouteNode- , rd_data :: Maybe a+ , rd_data :: !(V.Vector a) } deriving (Show, Eq) +instance NFData a => NFData (RouteData a) where+ rnf (RouteData n d) = rnf n `seq` rnf d+ data RoutingTree a = RoutingTree- { rt_node :: !(RouteData a)- , rt_children :: !(V.Vector (RoutingTree a))- }- deriving (Show, Eq)+ { rm_graph :: G.Graph+ , rm_nodeManager :: V.Vector (RouteData a)+ , rm_rootNode :: G.Node+ } deriving (Show, Eq) -buildRegex :: T.Text -> RegexWrapper-buildRegex t =- RegexWrapper (Regex.mkRegex $ T.unpack t) t+instance NFData a => NFData (RoutingTree a) where+ rnf (RoutingTree g v r) = rnf g `seq` rnf v `seq` rnf r emptyRoutingTree :: RoutingTree a emptyRoutingTree =- RoutingTree (RouteData RouteNodeRoot Nothing) V.empty+ let rootNode = 0+ nodeManager = V.singleton (RouteData RouteNodeRoot V.empty)+ in RoutingTree (G.addNode rootNode G.empty) nodeManager rootNode -mergeData :: Maybe a -> Maybe a -> Maybe a-mergeData (Just _) (Just _) =- error "Spock error: Don't define the same route twice!"-mergeData (Just a) _ = Just a-mergeData _ (Just b) = Just b-mergeData _ _ = Nothing+spawnNode :: G.Node -> RouteData a -> RoutingTree a -> (G.Node, RoutingTree a)+spawnNode parent nodeData rm =+ let nm' = V.snoc (rm_nodeManager rm) nodeData+ nodeId = (V.length nm') - 1+ g' = G.addNode nodeId (rm_graph rm)+ g'' = G.addEdge parent nodeId g'+ in (nodeId, RoutingTree g'' nm' (rm_rootNode rm)) +addActionToNode :: G.Node -> a -> RoutingTree a -> RoutingTree a+addActionToNode nodeId nodeAction rm =+ let routeDataOld = (rm_nodeManager rm) V.! nodeId+ routeDataNew =+ routeDataOld+ { rd_data = V.snoc (rd_data routeDataOld) nodeAction+ }+ nm' = V.modify (\v -> VM.write v nodeId routeDataNew) (rm_nodeManager rm)+ in rm { rm_nodeManager = nm' }+ addToRoutingTree :: T.Text -> a -> RoutingTree a -> RoutingTree a-addToRoutingTree route dat currTree =- let applyTree [] tree = tree- applyTree (current:xs) tree =- let children = V.map (\(RoutingTree d _) -> rd_node d) (rt_children tree)- currentDat =- case xs of- [] -> Just dat- _ -> Nothing- children' =- case V.findIndex (==current) children of- Nothing ->- let h = applyTree xs $ RoutingTree (RouteData current currentDat) V.empty- in V.cons h (rt_children tree)- Just idx ->- let origNode = (V.!) (rt_children tree) idx- matchingNode = rt_node $ origNode- appliedNode = matchingNode { rd_data = mergeData (rd_data matchingNode) currentDat }- in V.modify (\v -> VM.write v idx (applyTree xs $ RoutingTree appliedNode (rt_children origNode))) (rt_children tree)- in tree { rt_children = children' }- in case filter (not . T.null) $ T.splitOn "/" route of- [] ->- let currNode = rt_node currTree- currNode' = currNode { rd_data = mergeData (rd_data currNode) (Just dat) }- in currTree { rt_node = currNode' }- xs -> applyTree (map parseRouteNode xs) currTree+addToRoutingTree route action origRm =+ case chunks of+ [] ->+ addActionToNode (rm_rootNode origRm) action origRm+ _ ->+ treeTraversal (map parseRouteNode chunks) (rm_rootNode origRm) origRm+ where+ chunks = filter (not . T.null) $ T.splitOn "/" route+ treeTraversal [] _ rm = rm+ treeTraversal (node : xs) parentGraphNode rm =+ let graph = rm_graph rm+ children = G.children graph parentGraphNode+ nm = rm_nodeManager rm+ matchingChild =+ VU.find (\nodeId -> node == rd_node (nm V.! nodeId)) children+ in case matchingChild of+ Just childId ->+ treeTraversal xs childId (if null xs then addActionToNode childId action rm else rm)+ Nothing ->+ let (childId, rm') =+ spawnNode parentGraphNode (RouteData node (if null xs then V.singleton action else V.empty)) rm+ in treeTraversal xs childId rm' +matchRoute :: T.Text -> RoutingTree a -> [(ParamMap, a)]+matchRoute route globalMap =+ matchRoute' (T.splitOn "/" route) globalMap++matchRoute' :: [T.Text] -> RoutingTree a -> [(ParamMap, a)]+matchRoute' routeParts globalRm =+ findRoute (filter (not . T.null) routeParts) (rm_rootNode globalRm) emptyParamMap []+ where+ globalGraph = rm_graph globalRm+ nodeManager = rm_nodeManager globalRm++ findRoute [] parentId paramMap outMap =+ outMap ++ (V.toList $ V.map (\action -> (paramMap, action)) (rd_data (nodeManager V.! parentId)))+ findRoute (chunk : xs) parentId paramMap outMap =+ let children = G.children globalGraph parentId+ in VU.foldl' (\outV nodeId ->+ case matchNode chunk (rd_node $ nodeManager V.! nodeId) of+ (False, _) -> outV+ (True, mCapture) ->+ let paramMap' =+ case mCapture of+ Nothing -> paramMap+ Just (var, val) ->+ HM.insert var val paramMap+ in (findRoute xs nodeId paramMap' outMap) ++ outV+ ) [] children++buildRegex :: T.Text -> RegexWrapper+buildRegex t =+ RegexWrapper (Regex.mkRegex $ T.unpack t) t+ parseRouteNode :: T.Text -> RouteNode parseRouteNode node = case T.uncons node of@@ -163,48 +217,6 @@ emptyParamMap :: ParamMap emptyParamMap = HM.empty--matchRoute :: T.Text -> RoutingTree a -> [(ParamMap, a)]-matchRoute route globalTree =- matchRoute' (T.splitOn "/" route) globalTree--matchRoute' :: [T.Text] -> RoutingTree a -> [(ParamMap, a)]-matchRoute' routeParts globalTree =- case filter (not . T.null) routeParts of- [] ->- case rd_data $ rt_node globalTree of- Nothing -> []- Just action -> [(emptyParamMap, action)]- xs ->- findRoute xs (rt_children globalTree)- where- findRoute :: [T.Text] -> V.Vector (RoutingTree a) -> [(ParamMap, a)]- findRoute fullPath trees =- let foundPaths = V.foldl' (matchTree fullPath emptyParamMap) V.empty trees- in V.toList foundPaths- where- matchTree :: [T.Text] -> ParamMap -> V.Vector (ParamMap, a) -> RoutingTree a -> V.Vector (ParamMap, a)- matchTree [] _ vec _ = vec- matchTree (textNode : xs) paramMap vec rt =- case matchNode textNode (rd_node $ rt_node rt) of- (False, _) ->- vec- (True, mCapture) ->- let paramMap' =- case mCapture of- Nothing -> paramMap- Just (var, value) ->- HM.insert var value paramMap- nodeData = rd_data $ rt_node rt- nodeChildren = rt_children rt- in case xs of- [] | isJust nodeData ->- V.snoc vec (paramMap', fromJust nodeData)- | otherwise ->- vec- _ ->- let foundPaths = V.foldl' (matchTree xs paramMap') V.empty nodeChildren- in V.concat [foundPaths, vec] matchNode :: T.Text -> RouteNode -> (Bool, Maybe (CaptureVar, T.Text)) matchNode _ RouteNodeRoot = (False, Nothing)