packages feed

bpath-0.1.0: src/Path/Internal.hs

{-# LANGUAGE DeriveLift #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE QuasiQuotes #-}

module Path.Internal where

import Data.ByteString (ByteString)
import Data.Foldable
import Data.Maybe
import Data.Sequence
import Data.String
import Data.Void
import GHC.Generics (Generic)
import Instances.TH.Lift ()
import Language.Haskell.TH
import Language.Haskell.TH.Quote
import Language.Haskell.TH.Syntax
import Text.Megaparsec

type Parser = Parsec Void ByteString

data PathSeg
  = Parent
  | PathSeg {-# UNPACK #-} !ByteString
  deriving (Show, Eq, Lift, Generic)

fromPathSeg :: PathSeg -> ByteString
fromPathSeg Parent = ".."
fromPathSeg (PathSeg p) = p

pathSeg :: Parser (Maybe PathSeg)
pathSeg = try parentP <|> try dot <|> normalSeg
  where
    parentP = do
      _ <- single 46
      _ <- single 46
      pure (Just Parent)
    normalSeg = Just . PathSeg <$> takeWhile1P Nothing (/= 47)
    dot = do
      _ <- single 46
      pure Nothing

pathSeg' :: Parser (Maybe PathSeg)
pathSeg' = pathSeg <|> pure Nothing

relpath :: Parser (Path 'Rel)
relpath = do
  h <- pathSeg
  t <- many $ single 47 *> pathSeg'
  pure $ Path $ fromList $ catMaybes $ h : t

abspath :: Parser (Path 'Abs)
abspath = do
  _ <- single 47
  l <- sepBy pathSeg' $ single 47
  pure $ Path $ fromList $ catMaybes l

data PathType
  = Abs
  | Rel
  deriving (Show, Eq)

-- | A canonicalized file path
newtype Path (t :: PathType) = Path
  { unPath :: Seq PathSeg
  }
  deriving stock (Generic, Lift)
  deriving newtype (Eq, Show)

fromRel :: Path 'Rel -> ByteString
fromRel (fmap fromPathSeg . unPath -> l)
  | Empty <- l = "."
  | x :<| xs <- l = foldl' (\p s -> p <> "/" <> s) x xs

fromAbs :: Path 'Abs -> ByteString
fromAbs (fmap fromPathSeg . unPath -> l)
  | Empty <- l = "/"
  | x :<| xs <- l = "/" <> foldl' (\p s -> p <> "/" <> s) x xs

hush :: Either a b -> Maybe b
hush (Left _) = Nothing
hush (Right b) = Just b

parseRel :: ByteString -> Maybe (Path 'Rel)
parseRel = hush . runParser relpath ""

parseAbs :: ByteString -> Maybe (Path 'Abs)
parseAbs = hush . runParser abspath ""

mkAbs :: ByteString -> Q Exp
mkAbs = lift . fromMaybe (error "illformed absolute path") . parseAbs

mkRel :: ByteString -> Q Exp
mkRel = lift . fromMaybe (error "illformed relative path") . parseRel

qq :: (ByteString -> Q Exp) -> QuasiQuoter
qq quoteExp' =
  QuasiQuoter
    { quoteExp = quoteExp' . fromString,
      quotePat = \_ ->
        fail "illegal QuasiQuote (allowed as expression only, used as a pattern)",
      quoteType = \_ ->
        fail "illegal QuasiQuote (allowed as expression only, used as a type)",
      quoteDec = \_ ->
        fail "illegal QuasiQuote (allowed as expression only, used as a declaration)"
    }

absp :: QuasiQuoter
absp = qq mkAbs

relp :: QuasiQuoter
relp = qq mkRel

(</>) :: Path t -> Path 'Rel -> Path t
(Path p1) </> (Path p2) = Path (p1 <> p2)

stripPrefix :: Path t -> Path t -> Maybe (Path 'Rel)
stripPrefix (Path p1) (Path p2) =
  Path <$> stripPrefix' p1 p2
  where
    stripPrefix' Empty p = pure p
    stripPrefix' _ Empty = Nothing
    stripPrefix' (x :<| xs) (y :<| ys)
      | x == y = stripPrefix' xs ys
      | otherwise = Nothing

isPrefixOf :: Path t -> Path t -> Bool
isPrefixOf p1 p2 = isJust $ stripPrefix p1 p2

parent :: Path t -> Path t
parent (Path Empty) = Path Empty
parent (Path (xs :|> _)) = Path xs

filename :: Path t -> Path 'Rel
filename (Path Empty) = Path Empty
filename (Path (Empty :|> x)) = Path $ pure x
filename (Path (_ :<| xs)) = filename $ Path xs