packages feed

path-extra-0.3.0: src/Path/Extended.hs

{-# LANGUAGE
    MultiParamTypeClasses
  , FunctionalDependencies
  , NamedFieldPuns
  , ScopedTypeVariables
  , OverloadedStrings
  , QuasiQuotes
  , DeriveGeneric
  #-}

module Path.Extended
  ( -- * Types
    Location (..)
  , LocationPath (..)
  , QueryParam
  , -- * Classes
    ToPath (..)
  , ToLocation (..)
  , FromPath (..)
  , FromLocation (..)
  , -- * Combinators
    -- ** Path
    fromDir
  , fromFile
  , prependAbs
  , prependRel
  , -- ** Query Parameters
    setQuery
  , addQuery
  , (<&>)
  , addQueries
  , delQuery
  , getQuery
  , -- ** Fragment
    setFragment
  , addFragment
  , (<#>)
  , delFragment
  , getFragment
  , -- ** Parser & Printer
    locationAbsParser
  , locationRelParser
  , printLocation
  ) where

-- import Path as P hiding ((</>))
import Path (Path, Abs, Rel, Dir, File, (</>), toFilePath, parseAbsFile, parseAbsDir, parseRelDir, parseRelFile, stripProperPrefix, absdir, reldir)

import Prelude hiding (takeWhile)
import Data.Attoparsec.Text (Parser, char, takeWhile, takeWhile1, sepBy)
import qualified Data.Text as T
import Control.Applicative ((<|>), optional)
import Control.Exception (SomeException)
import Control.Monad (void)
import GHC.Generics (Generic)



-- | Convenience typeclass for symbolic, stringless routes - make an instance
-- for your own data type to use your constructors as route-referencing symbols.
class ToPath sym base type' | sym -> base type' where
  toPath :: sym -> Path base type'

-- | Convenience typeclass for symbolic, stringless routes - make an instance
-- for your own data type to use your constructors as route-referencing symbols.
class ToLocation sym base | sym -> base where
  toLocation :: sym -> Location base

class FromPath sym base type' | sym -> base type' where
  parsePath :: Path base type' -> Either String sym

class FromLocation sym base | sym -> base where
  parseLocation :: Location base -> Either String sym


data LocationPath base
  = Dir (Path base Dir)
  | File (Path base File)
  deriving (Eq, Ord, Generic)

-- | A location for some base and type - internally uses @Path@.
data Location base = Location
  { locPath        :: LocationPath base
  , locQueryParams :: [QueryParam]
  , locFragment    :: Maybe String
  } deriving (Eq, Ord, Generic)



fromDir :: Path base Dir -> Location base
fromDir path = Location
  { locPath = Dir path
  , locQueryParams = []
  , locFragment = Nothing
  }

fromFile :: Path base File -> Location base
fromFile path = Location
  { locPath = File path
  , locQueryParams = []
  , locFragment = Nothing
  }


locationAbsParser :: Parser (Location Abs)
locationAbsParser = do
  divider
  locPath <- do
    xs <- chunk `sepBy` divider
    case xs of
      [] -> pure $ Dir [absdir|/|]
      _ -> do
        let dir = do
              divider
              case parseAbsDir (T.unpack ("/" <> T.intercalate "/" xs <> "/")) of
                Left (e :: SomeException) -> fail (show e)
                Right x -> pure (Dir x)
            file =
              case parseAbsFile (T.unpack ("/" <> T.intercalate "/" xs)) of
                Left (e :: SomeException) -> fail (show e)
                Right x -> pure (File x)
        dir <|> file
  locQueryParams <- do
    xs <- optional $ do
      let val = T.unpack <$> takeWhile (`notElem` ['=','&','#'])
      void (char '?')
      let kv = do
            k <- val
            mV <- optional $ do
              void (char '=')
              val
            pure (k,mV)
      kv `sepBy` void (char '&')
    case xs of
      Nothing -> pure []
      Just xs' -> pure xs'
  locFragment <- optional $ do
    void (char '#')
    xs <- takeWhile (const True)
    pure (T.unpack xs)
  pure Location
    { locPath
    , locQueryParams
    , locFragment
    }
  where
    divider = void (char '/')
    chunk = takeWhile1 (`notElem` ['?','&','/','#'])

locationRelParser :: Parser (Location Rel)
locationRelParser = do
  locPath <- do
    xs <- chunk `sepBy` divider
    case xs of
      [] -> pure $ Dir [reldir|./|]
      _ -> do
        let dir = do
              divider
              case parseRelDir (T.unpack (T.intercalate "/" xs <> "/")) of
                Left (e :: SomeException) -> fail (show e)
                Right x -> pure (Dir x)
            file =
              case parseRelFile (T.unpack (T.intercalate "/" xs)) of
                Left (e :: SomeException) -> fail (show e)
                Right x -> pure (File x)
        dir <|> file
  locQueryParams <- do
    xs <- optional $ do
      let val = T.unpack <$> takeWhile (`notElem` ['=','&','#'])
      void (char '?')
      let kv = do
            k <- val
            mV <- optional $ do
              void (char '=')
              val
            pure (k,mV)
      kv `sepBy` void (char '&')
    case xs of
      Nothing -> pure []
      Just xs' -> pure xs'
  locFragment <- optional $ do
    void (char '#')
    xs <- takeWhile (const True)
    pure (T.unpack xs)
  pure Location
    { locPath
    , locQueryParams
    , locFragment
    }
  where
    divider = void (char '/')
    chunk = takeWhile1 (`notElem` ['?','&','/','#'])


prependAbs :: Path Abs Dir -> Location Abs -> Location Abs
prependAbs path l@Location{locPath} =
  case locPath of
    File f ->
      l { locPath = case stripProperPrefix [absdir|/|] f of
            Nothing -> undefined
            Just f' -> File (path </> f')
        }
    Dir d ->
      l { locPath = case stripProperPrefix [absdir|/|] d of
            Nothing -> undefined
            Just d' -> Dir (path </> d')
        }

prependRel :: Path Rel Dir -> Location Rel -> Location Rel
prependRel path l@Location{locPath} =
  case locPath of
    File f ->
      l { locPath = File (path </> f)
        }
    Dir d ->
      l { locPath = Dir (path </> d)
        }


printLocation :: Location base -> T.Text
printLocation (Location pa qp fr) =
  let loc = case pa of
        Dir x -> toFilePath x
        File x -> toFilePath x
      query = case qp of
                [] -> ""
                qs -> "?" <> T.intercalate "&" (go <$> qs)
        where
          go (k,mv) = T.pack k <> maybe "" (\v -> "=" <> T.pack v) mv
  in T.pack loc <> query <> maybe "" (\f -> "#" <> T.pack f) fr

type QueryParam = (String, Maybe String)



setQuery :: [QueryParam] -> Location base -> Location base
setQuery qp (Location pa _ fr) =
  Location pa qp fr

-- | Appends a query parameter
addQuery :: QueryParam -> Location base -> Location base
addQuery q (Location pa qp fr) =
  Location pa (qp ++ [q]) fr

(<&>) :: Location base -> QueryParam -> Location base
(<&>) = flip addQuery

infixl 7 <&>

addQueries :: [QueryParam] -> Location base -> Location base
addQueries qs (Location pa qs' fr) =
  Location pa (qs' ++ qs) fr

delQuery :: Location base -> Location base
delQuery = setQuery []

getQuery :: Location base -> [QueryParam]
getQuery (Location _ qp _) =
  qp


setFragment :: Maybe String -> Location base -> Location base
setFragment fr (Location pa qp _) =
  Location pa qp fr

addFragment :: String -> Location base -> Location base
addFragment fr = setFragment (Just fr)

(<#>) :: Location base -> String -> Location base
(<#>) = flip addFragment

infixl 8 <#>

delFragment :: Location base -> Location base
delFragment = setFragment Nothing

getFragment :: Location base -> Maybe String
getFragment (Location _ _ x) = x