{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_HADDOCK hide #-}
module Airship.Internal.Route
( RoutingSpec
, Route
, RouteLeaf
, RoutedResource(..)
, Trie
, root
, var
, star
, (</>)
, (#>)
, (#>=)
, runRouter
, route
, routeText
) where
import Airship.Resource as Resource
import Control.Monad.Writer.Class (MonadWriter, tell)
import qualified Data.ByteString as B
import qualified Data.ByteString.Base64 as Base64
import qualified Data.ByteString.Char8 as BC8
import Data.HashMap.Strict (HashMap, fromList)
import qualified Data.List as L (foldl')
import Data.Maybe (isNothing)
import Data.Semigroup (Semigroup, (<>))
import Data.Monoid (Monoid)
import Data.Text (Text)
import qualified Data.Text as T (intercalate, cons)
import Data.Text.Encoding (encodeUtf8, decodeUtf8)
import Data.Trie (Trie)
import qualified Data.Trie as Trie
#if __GLASGOW_HASKELL__ < 710
import Control.Applicative
#endif
import Control.Monad.Writer (Writer, WriterT (..), execWriter)
import Data.String (IsString, fromString)
newtype Route = Route { getRoute :: [BoundOrUnbound] } deriving (Show, Semigroup, Monoid)
routeText :: Route -> Text
routeText (Route parts) =
T.cons '/' $ T.intercalate "/" ((boundOrUnboundText <$> parts))
data BoundOrUnbound = Bound Text
| Var Text
| RestUnbound deriving (Show)
boundOrUnboundText :: BoundOrUnbound -> Text
boundOrUnboundText (Bound t) = t
boundOrUnboundText (Var t) = ":" <> t
boundOrUnboundText (RestUnbound) = "*"
instance IsString Route where
fromString s = Route [Bound (fromString s)]
data RoutedResource m
= RoutedResource Route (Resource m)
data RouteLeaf m = RouteMatch (RoutedResource m) [Text]
| RVar
| RouteMatchOrVar (RoutedResource m) [Text]
| Wildcard (RoutedResource m)
runRouter :: RoutingSpec m a -> Trie (RouteLeaf m)
runRouter routes = toTrie $ execWriter (getRouter routes)
where
toTrie = L.foldl' insertOrReplace Trie.empty
insertOrReplace t (k, v) =
let newV = maybe v (mergeValues v) $ Trie.lookup k t
in Trie.insert k newV t
mergeValues (Wildcard x) _ = Wildcard x
mergeValues _ (Wildcard x) = Wildcard x
mergeValues RVar RVar = RVar
mergeValues RVar (RouteMatch x y) = RouteMatchOrVar x y
mergeValues (RouteMatch _ _) (RouteMatch x y) = RouteMatch x y
mergeValues (RouteMatch x y) RVar = RouteMatchOrVar x y
mergeValues (RouteMatchOrVar _ _) (RouteMatch x y) = RouteMatchOrVar x y
mergeValues (RouteMatchOrVar x y) _ = RouteMatchOrVar x y
mergeValues _ v = v
(</>) :: Route -> Route -> Route
(</>) = (<>)
root :: Route
root = Route []
var :: Text -> Route
var t = Route [Var t]
star :: Route
star = Route [RestUnbound]
(#>) :: MonadWriter [(B.ByteString, (RouteLeaf a))] m
=> Route -> Resource a -> m ()
k #> v = do
let (key, routes, vars, isWild) = foldl routeFoldFun ("", [], [], False) (getRoute k)
key' = if BC8.null key then "/"
else key
ctor = if isWild
then Wildcard (RoutedResource k v)
else RouteMatch (RoutedResource k v) vars
tell $ (key', ctor) : routes
where
routeFoldFun (kps, rt, vs, False) (Bound x) =
(B.concat [kps, "/", encodeUtf8 x], rt, vs, False)
routeFoldFun (kps, rt, vs, False) (Var x) =
let partKey = Base64.encode $ B.concat [kps, "var"]
rt' = (kps, RVar) : rt
in (partKey, rt', x:vs, False)
routeFoldFun (kps, rt, vs, False) RestUnbound =
(kps, rt, vs, True)
routeFoldFun (kps, rt, vs, True) _ =
(kps, rt, vs, True)
(#>=) :: MonadWriter [(B.ByteString, (RouteLeaf a))] m
=> Route -> m (Resource a) -> m ()
k #>= mv = mv >>= (k #>)
newtype RoutingSpec m a = RoutingSpec {
getRouter :: Writer [(B.ByteString, RouteLeaf m)] a
} deriving ( Functor, Applicative, Monad
, MonadWriter [(B.ByteString, RouteLeaf m)]
)
route :: Trie (RouteLeaf a)
-> BC8.ByteString
-> Maybe (RoutedResource a, (HashMap Text Text, [Text]))
route routes pInfo = let matchRes = Trie.match routes pInfo
in matchRoute' routes matchRes mempty Nothing
matchRoute' :: Trie (RouteLeaf a)
-> Maybe (B.ByteString, RouteLeaf a, B.ByteString)
-> [Text]
-> Maybe B.ByteString
-> Maybe (RoutedResource a, (HashMap Text Text, [Text]))
matchRoute' _routes Nothing _ps _dsp =
Nothing
matchRoute' routes (Just (matched, RouteMatchOrVar r vars, "")) ps dsp =
matchRoute' routes (Just (matched, RouteMatch r vars, "")) ps dsp
matchRoute' _routes (Just (matched, RouteMatch r vars, "")) ps dsp =
Just (r, (fromList $ zip vars ps, dispatchList dsp matched))
where
dispatchList (Just d) m = toTextList $ B.concat [d, m]
dispatchList Nothing _ = mempty
toTextList bs = decodeUtf8 <$> BC8.split '/' bs
matchRoute' _routes (Just (_matched, RouteMatch _r _vars, _)) _ps _dsp =
Nothing
matchRoute' routes (Just (matched, RouteMatchOrVar _r _vars, rest)) ps dsp =
matchRoute' routes (Just (matched, RVar, rest)) ps dsp
matchRoute' routes (Just (matched, RVar, rest)) ps dsp
| BC8.null rest = Nothing
| BC8.take 2 rest == "//" = Nothing
| BC8.head rest == '/' =
let nextKey = B.concat [ Base64.encode $ B.concat [matched, "var"]
, BC8.dropWhile (/='/') $ BC8.dropWhile (=='/') rest
]
updDsp = if isNothing dsp then Just mempty
else dsp
paramVal = decodeUtf8 . BC8.takeWhile (/='/')
$ BC8.dropWhile (=='/') rest
matchRes = Trie.match routes nextKey
in matchRoute' routes matchRes (paramVal:ps) updDsp
| otherwise = Nothing
matchRoute' _routes (Just (_matched, Wildcard r, rest)) _ps _dsp =
Just (r, (mempty, decodeUtf8 <$> [BC8.dropWhile (=='/') rest]))