{-# LANGUAGE CPP, FlexibleInstances, TypeSynonymInstances, TupleSections #-}
#if __GLASGOW_HASKELL__ > 702
{-# LANGUAGE DefaultSignatures, OverloadedStrings, ScopedTypeVariables, TypeOperators #-}
#endif
module Web.Routes.PathInfo
( stripOverlap
, stripOverlapBS
, stripOverlapText
, URLParser
, pToken
, segment
, anySegment
, patternParse
, parseSegments
, PathInfo(..)
, toPathInfo
, toPathInfoParams
, fromPathInfo
, fromPathInfoParams
, mkSitePI
, showParseError
#if __GLASGOW_HASKELL__ > 702
, Generic
#endif
) where
import Blaze.ByteString.Builder (Builder, toByteString)
import Control.Applicative ((<$>), (<*))
import Control.Monad (msum)
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Int (Int64)
import Data.List as List (stripPrefix, tails)
import Data.Text as Text (Text, pack, unpack, null, tails, stripPrefix)
import Data.Text.Encoding (decodeUtf8)
import Data.Text.Read (decimal, signed)
import Data.Maybe (fromJust)
import Network.HTTP.Types
import Text.ParserCombinators.Parsec.Combinator (notFollowedBy)
import Text.ParserCombinators.Parsec.Error (ParseError, errorPos, errorMessages, showErrorMessages)
import Text.ParserCombinators.Parsec.Pos (incSourceLine, sourceName, sourceLine, sourceColumn)
import Text.ParserCombinators.Parsec.Prim ((<?>), GenParser, getInput, setInput, getPosition, token, parse, many)
import Web.Routes.Base (decodePathInfoParams, decodePathInfo, encodePathInfo)
import Web.Routes.Site (Site(..))
#if __GLASGOW_HASKELL__ > 702
import Control.Applicative ((<$), (<*>), (<|>), pure)
import Data.Char (toLower, isUpper)
import Data.List (intercalate)
import Data.List.Split (split, dropInitBlank, keepDelimsL, whenElt)
import GHC.Generics
#endif
stripOverlap :: (Eq a) => [a] -> [a] -> [a]
stripOverlap x y = fromJust $ msum $ [ List.stripPrefix p y | p <- List.tails x]
stripOverlapText :: Text -> Text -> Text
stripOverlapText x y = fromJust $ msum $ [ Text.stripPrefix p y | p <- Text.tails x ]
stripOverlapBS :: B.ByteString -> B.ByteString -> B.ByteString
stripOverlapBS x y = fromJust $ msum $ [ stripPrefix p y | p <- B.tails x ]
where
stripPrefix :: B.ByteString -> B.ByteString -> Maybe B.ByteString
stripPrefix x y
| x `B.isPrefixOf` y = Just $ B.drop (B.length x) y
| otherwise = Nothing
type URLParser a = GenParser Text () a
pToken :: tok -> (Text -> Maybe a) -> URLParser a
pToken msg f = do pos <- getPosition
token unpack (const $ incSourceLine pos 1) f
segment :: Text -> URLParser Text
segment x = (pToken (const x) (\y -> if x == y then Just x else Nothing)) <?> unpack x
anySegment :: URLParser Text
anySegment = pToken (const "any string") Just
eof :: URLParser ()
eof = notFollowedBy anySegment <?> "end of input"
patternParse :: ([Text] -> Either String a) -> URLParser a
patternParse p =
do segs <- getInput
case p segs of
(Right r) ->
do setInput []
return r
(Left err) -> fail err
showParseError :: ParseError -> String
showParseError pErr =
let pos = errorPos pErr
posMsg = sourceName pos ++ " (segment " ++ show (sourceLine pos) ++ " character " ++ show (sourceColumn pos) ++ "): "
msgs = errorMessages pErr
in posMsg ++ showErrorMessages "or" "unknown parse error" "expecting" "unexpected" "end of input" msgs
parseSegments :: URLParser a -> [Text] -> Either String a
parseSegments p segments =
case parse (p <* eof) (show segments) segments of
(Left e) -> Left (showParseError e)
(Right r) -> Right r
#if __GLASGOW_HASKELL__ > 702
hyphenate :: String -> Text
hyphenate =
pack . intercalate "-" . map (map toLower) . split splitter
where
splitter = dropInitBlank . keepDelimsL . whenElt $ isUpper
class GPathInfo f where
gtoPathSegments :: f url -> [Text]
gfromPathSegments :: URLParser (f url)
instance GPathInfo U1 where
gtoPathSegments U1 = []
gfromPathSegments = pure U1
instance GPathInfo a => GPathInfo (D1 c a) where
gtoPathSegments = gtoPathSegments . unM1
gfromPathSegments = M1 <$> gfromPathSegments
instance GPathInfo a => GPathInfo (S1 c a) where
gtoPathSegments = gtoPathSegments . unM1
gfromPathSegments = M1 <$> gfromPathSegments
instance forall c a. (GPathInfo a, Constructor c) => GPathInfo (C1 c a) where
gtoPathSegments m@(M1 x) = (hyphenate . conName) m : gtoPathSegments x
gfromPathSegments = M1 <$ segment (hyphenate . conName $ (undefined :: C1 c a r))
<*> gfromPathSegments
instance (GPathInfo a, GPathInfo b) => GPathInfo (a :*: b) where
gtoPathSegments (a :*: b) = gtoPathSegments a ++ gtoPathSegments b
gfromPathSegments = (:*:) <$> gfromPathSegments <*> gfromPathSegments
instance (GPathInfo a, GPathInfo b) => GPathInfo (a :+: b) where
gtoPathSegments (L1 x) = gtoPathSegments x
gtoPathSegments (R1 x) = gtoPathSegments x
gfromPathSegments = L1 <$> gfromPathSegments
<|> R1 <$> gfromPathSegments
instance PathInfo a => GPathInfo (K1 i a) where
gtoPathSegments = toPathSegments . unK1
gfromPathSegments = K1 <$> fromPathSegments
#endif
class PathInfo url where
toPathSegments :: url -> [Text]
fromPathSegments :: URLParser url
#if __GLASGOW_HASKELL__ > 702
default toPathSegments :: (Generic url, GPathInfo (Rep url)) => url -> [Text]
toPathSegments = gtoPathSegments . from
default fromPathSegments :: (Generic url, GPathInfo (Rep url)) => URLParser url
fromPathSegments = to <$> gfromPathSegments
#endif
toPathInfo :: (PathInfo url) => url -> Text
toPathInfo = decodeUtf8 . toByteString . toPathInfoUtf8
toPathInfoUtf8 :: (PathInfo url) => url -> Builder
toPathInfoUtf8 = flip encodePath [] . toPathSegments
toPathInfoParams :: (PathInfo url) =>
url
-> [(Text, Maybe Text)]
-> Text
toPathInfoParams url params = encodePathInfo (toPathSegments url) params
fromPathInfo :: (PathInfo url) => ByteString -> Either String url
fromPathInfo pi =
parseSegments fromPathSegments (decodePathInfo $ dropSlash pi)
fromPathInfoParams :: (PathInfo url) => ByteString -> Either String (url, [(Text, Maybe Text)])
fromPathInfoParams pi =
(,query) <$> parseSegments fromPathSegments url
where
(url, query) = decodePathInfoParams $ dropSlash pi
dropSlash :: ByteString -> ByteString
dropSlash s =
if ((B.singleton '/') `B.isPrefixOf` s)
then B.tail s
else s
mkSitePI :: (PathInfo url) =>
((url -> [(Text, Maybe Text)] -> Text) -> url -> a)
-> Site url a
mkSitePI handler =
Site { handleSite = handler
, formatPathSegments = (\x -> (x, [])) . toPathSegments
, parsePathSegments = parseSegments fromPathSegments
}
instance PathInfo Text where
toPathSegments = (:[])
fromPathSegments = anySegment
instance PathInfo [Text] where
toPathSegments = id
fromPathSegments = many anySegment
instance PathInfo String where
toPathSegments = (:[]) . pack
fromPathSegments = unpack <$> anySegment
instance PathInfo [String] where
toPathSegments = id . map pack
fromPathSegments = many (unpack <$> anySegment)
instance PathInfo Int where
toPathSegments i = [pack $ show i]
fromPathSegments = pToken (const "Int") checkIntegral
instance PathInfo Integer where
toPathSegments i = [pack $ show i]
fromPathSegments = pToken (const "Integer") checkIntegral
instance PathInfo Int64 where
toPathSegments i = [pack $ show i]
fromPathSegments = pToken (const "Int64") checkIntegral
checkIntegral :: Integral a => Text -> Maybe a
checkIntegral txt =
case signed decimal txt of
(Left e) -> Nothing
(Right (n, r))
| Text.null r -> Just n
| otherwise -> Nothing