packages feed

hexpress-0.1.0.0: src/Network/Hexpress/Middleware/Router.hs

module Network.Hexpress.Middleware.Router (
Method(..)
, router
, standaloneRouter
) where

import qualified Data.ByteString.Char8 as SB
import qualified Data.Text as TXT
import Network.Hexpress.Types
import Network.Hexpress.Request
import Network.Hexpress.Server
import Network.HTTP.Types.URI
import Network.HTTP.Types.Status

data Method = GET
  | HEAD
  | POST
  | PUT
  | DELETE
  | CONNECT
  | OPTIONS
  | TRACE
  | PATCH
  deriving (Eq)

methodToString :: Method -> String
methodToString GET = "GET"
methodToString POST = "POST"
methodToString PUT = "PUT"
methodToString PATCH = "PATCH"
methodToString DELETE = "DELETE"
methodToString HEAD = "HEAD"
methodToString CONNECT = "CONNECT"
methodToString OPTIONS = "OPTIONS"
methodToString TRACE = "TRACE"

stringToMethod :: String -> Method
stringToMethod "GET" = GET
stringToMethod "POST" = POST
stringToMethod "PUT" = PUT
stringToMethod "PATCH" = PATCH
stringToMethod "DELETE" = DELETE
stringToMethod "HEAD" = HEAD
stringToMethod "CONNECT" = CONNECT
stringToMethod "OPTIONS" = OPTIONS
stringToMethod "TRACE" = TRACE

emptyText :: TXT.Text
emptyText = TXT.pack ""

starText :: TXT.Text
starText = TXT.pack "*"

isColon :: TXT.Text -> Bool
isColon str = TXT.head str == ':'

byteStringToMethod :: SB.ByteString -> Method
byteStringToMethod bs = stringToMethod $ SB.unpack bs

stringToPath :: String -> [TXT.Text]
stringToPath str = decodePathSegments $ SB.pack str

type Route a b = (Method, [TXT.Text], (a -> Server b))

extractRtPath :: Route a b -> [TXT.Text]
extractRtPath (_, p, _) = p

isEmpty :: [a] -> Bool
isEmpty [] = True
isEmpty _ = False

isMatch :: [TXT.Text] -> Route a b -> Bool
isMatch [] (_, [], _) = True
isMatch [] (_,(x:xs),_) = False
isMatch (y:ys) (_, [], _) = False
isMatch (y:ys) (m,(x:xs),fn)
  | x == emptyText || x == starText = True
  | isColon x = isMatch ys (m,xs,fn)
  | x == y = isMatch ys (m,xs,fn)
  | otherwise = False

isMethodMatch :: Method -> Route a b -> Bool
isMethodMatch targetMeth (meth, _, _) = meth == targetMeth

-- assumes non empty
getLast :: [a] -> a
getLast [x] = x
getLast (x:xs) = getLast xs

isExact :: Route a b -> Bool
isExact (_, [], _) = True
isExact (m, (x:xs), fn)
  | x == starText || x == emptyText || isColon x = False
  | otherwise = isExact (m, xs, fn)

findLongest :: Int -> [Route a b] -> [Route a b] -> Route a b
findLongest largest ls [] = getLast ls
findLongest largest ls (rt@(m,x,fn):xs)
  | isExact rt = rt
  | length x > largest = findLongest (length x) [rt] xs
  | length x == largest = findLongest largest (rt:ls) xs
  | otherwise = findLongest largest ls xs

router :: [(Method, String, a -> Server b)] -> (a -> Server b)
router rts arg = routerHelper where
  preprocessed = map (\(m, str, fn) -> (m, stringToPath str, fn)) rts
  routerHelper = do
    path <- getPath
    let matches = filter (isMatch path) preprocessed
    if isEmpty matches then notFound arg
    else do
      meth <- getMethod
      let targetMeth = byteStringToMethod meth
      let methMatches = filter (isMethodMatch targetMeth) matches
      if isEmpty methMatches then notAllowed arg
      else do
        let (_, _, fn) = findLongest (-1) [] methMatches
        fn arg


notFound :: a -> Server b
notFound _ = do
  setStatus status404
  end

notAllowed :: a -> Server b
notAllowed _ = do
  setStatus status405
  end

standaloneRouter :: [(Method, String, Server a)] -> Server a
standaloneRouter rts = (router $ map (\(meth, rt, handler) -> (meth, rt, \() -> handler)) rts) ()