packages feed

webapp-0.2.0: Web/App/Path.hs

{-|
Module      : Web.App.Path
Copyright   : (c) Nathaniel Symer, 2015
License     : MIT
Maintainer  : nate@symer.io
Stability   : experimental
Portability : POSIX

A URI pathInfo wrapper.
-}

{-# LANGUAGE OverloadedStrings #-}

module Web.App.Path
(
  -- * Types
  Path,
  PathInfo,
  -- * Path Constructors
  literal,
  captured,
  regex,
  -- * Path Operations
  pathMatches,
  -- * Text-Based Path Operations
  isRoot,
  splitPath,
  mkPathInfo,
  joinPath,
  pathCaptures
)
where

import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.ByteString as B
import Text.Regex.Posix
import Data.List
import Data.String

-- |Represents a PathInfo (e.g. from WAI)
type PathInfo = [Text]

-- |Describes a matchable path.
data Path = LiteralPath PathInfo -- ^ Match a path as-is
          | CapturedPath PathInfo -- ^ Match a path with wildcards
          | RegexPath Regex -- ^ Match a path with a regex
          
instance IsString Path where
  fromString = f . mkPathInfo . T.pack
    where f pinfo = case find (T.isPrefixOf ":") pinfo of
            Just _ -> CapturedPath pinfo
            Nothing -> LiteralPath pinfo
      
-- |Construct a literal 'Path'.
literal :: Text -> Path
literal = LiteralPath . mkPathInfo

-- |Construct a captured 'Path'.
captured :: Text -> Path
captured = CapturedPath . mkPathInfo

-- |Construct a regex 'Path'.
regex :: Text -> Path
regex = RegexPath . makeRegex . T.encodeUtf8

-- |Returns @True@ if the given 'Path' matches the given 'PathInfo'
pathMatches :: Path -> PathInfo -> Bool
pathMatches (RegexPath ex) pin = matchTest ex $ T.encodeUtf8 $ joinPath pin
pathMatches (LiteralPath pin) pin2 = pin == pin2
pathMatches (CapturedPath pin) pin2 = f pin pin2
  where f [] [] = True
        f _  [] = False
        f [] _  = False
        f (c:cs) (p:ps)
          | T.head c == ':' = f cs ps
          | p == c = f cs ps
          | otherwise = False

-- |Returns true if given path is the root.
isRoot :: Path -> Bool
isRoot (RegexPath ex) = matchTest ex ("/" :: String)
isRoot (LiteralPath pin) = null pin
isRoot (CapturedPath pin) = null pin

{- General path operations -}

-- |Splits path into (path,queryString).
splitPath :: Text -- ^ path
          -> (Text,Text)
splitPath pth = (p,T.drop 1 q)
  where (p,q) = T.break (== '?') pth

-- |Split @path@ into a pathInfo list.
mkPathInfo :: Text -- ^ path
           -> PathInfo
mkPathInfo = filter (not . T.null) . T.splitOn "/" . fst . splitPath

-- |Join pathInfo into a 'Text'ual path.
joinPath :: PathInfo -- ^ pathInfo
         -> Text
joinPath = mconcat . (:) "/" . intersperse "/"

{- Captures & Regex Captures -}
      
-- |Returns path captures by comparing @path@ to @pathInfo@.
pathCaptures :: Path -- ^ path
             -> PathInfo -- ^ pathInfo
             -> [(Text,Text)]
pathCaptures (LiteralPath _) _ = []
pathCaptures (RegexPath r) pin = maybe [] (\(_,x,_,xs) -> f (x:xs)) matched
  where
    f = numberList . map T.decodeUtf8
    matched :: Maybe (B.ByteString,B.ByteString,B.ByteString,[B.ByteString])
    matched = matchM r $ T.encodeUtf8 $ joinPath pin

pathCaptures (CapturedPath cap) pin = f [] cap pin
  where
    f acc [] [] = acc
    f _   _  [] = []
    f _   [] _  = []
    f acc (c:cs) (p:ps)
      | T.head c == ':' = f ((T.tail c,p):acc) cs ps
      | p == c = f acc cs ps
      | otherwise = []
      
numberList :: [Text] -> [(Text,Text)]
numberList = zipWith (\a b -> (T.pack $ show a, b)) ([0..] :: [Integer])