path-tagged-0.1.0.0: src/Path/Tagged.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveLift #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RoleAnnotations #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE StandaloneKindSignatures #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
module Path.Tagged (
-- * Types
PathTo (..),
ToSubpath,
retagPath,
Base (..),
Untag,
Abs,
RelTo,
File,
Dir,
-- ** Special Entity Tags
Unknown,
Subpath (..),
EntryOf,
Entry,
Parent,
ParentOf,
SomeBase (..),
untagSomeBase,
-- * Exceptions
PathException (..),
-- * QuasiQuotes
absdir,
reldir,
absfile,
relfile,
-- * Operations
(</>),
stripProperPrefix,
isProperPrefixOf,
replaceProperPrefix,
replaceProperPrefix',
parent,
filename,
dirname,
addExtension,
splitExtension,
fileExtension,
replaceExtension,
mapSomeBase,
prjSomeBase,
-- * Parsing
parseAbsDir,
parseRelDir,
parseAbsFile,
parseRelFile,
parseSomeDir,
parseSomeFile,
-- * Conversion
toFilePath,
fromAbsDir,
fromRelDir,
fromAbsFile,
fromRelFile,
fromSomeDir,
fromSomeFile,
-- * TemplateHaskell constructors
mkAbsDir,
mkRelDir,
mkAbsFile,
mkRelFile,
-- ** Typed constructors
mkAbsDirT,
mkRelDirT,
mkAbsFileT,
mkRelFileT,
) where
import Control.DeepSeq (NFData)
import Control.Monad.Catch (MonadThrow)
import Data.Aeson (FromJSON, FromJSONKey, ToJSON, Value)
import qualified Data.Aeson.Types as J
import qualified Data.Bifunctor as Bi
import Data.Coerce (coerce)
import Data.Functor ((<&>))
import Data.Hashable (Hashable)
import Data.Kind (Type)
import Data.Maybe (isJust)
import GHC.Generics (Generic)
import GHC.TypeLits
import Language.Haskell.TH (ExpQ)
import Language.Haskell.TH.Quote (QuasiQuoter (..))
import Language.Haskell.TH.Syntax (Lift)
import Language.Haskell.TH.Syntax.Compat
import Path (Dir, File, PathException)
import qualified Path as P
import Type.Reflection (Typeable)
type role PathTo phantom nominal nominal
type PathTo :: forall {k}. k -> Base k -> Type -> Type
newtype PathTo entity pk t = PathTo {untagPath :: P.Path (Untag pk) t}
deriving (Generic)
deriving newtype (Eq, Ord, Show, Hashable, ToJSON, NFData)
deriving instance (Typeable t, Typeable (Untag b)) => Lift (PathTo e b t)
-- | Unknown base directory
type Unknown :: k
type family Unknown where
type EntryOf :: forall {k}. Subpath k -> k
type family EntryOf k where
EntryOf ('Entry e) = e
EntryOf p = TypeError ('Text "Concrete entry expected, but got: " ':<>: 'ShowType p)
data Subpath k = Entry k | Parent (Subpath k)
type Entry = 'Entry
type Parent = 'Parent
type ParentOf e = 'Parent ('Entry e)
type Retagged' :: forall k k' -> Base k -> Base k'
type family Retagged' k k' bk where
Retagged' k k a = a
Retagged' k k' Abs = Abs
Retagged' k k' (RelTo p) =
TypeError ('Text "Different kind of path cannot be rettaged with relative base: " ':<>: 'ShowType (RelTo p))
type Retagged :: forall {k} {k'}. Base k -> Base k'
type Retagged (a :: Base k) = Retagged' k k' a :: Base k'
retagPath :: PathTo p b t -> PathTo p' (Retagged b) t
retagPath = coerce
deriving newtype instance FromJSON (PathTo k (RelTo b) File)
deriving newtype instance FromJSON (PathTo k (RelTo b) Dir)
deriving newtype instance FromJSON (PathTo k Abs File)
deriving newtype instance FromJSON (PathTo k Abs Dir)
deriving newtype instance FromJSONKey (PathTo k (RelTo b) File)
deriving newtype instance FromJSONKey (PathTo k (RelTo b) Dir)
deriving newtype instance FromJSONKey (PathTo k Abs File)
deriving newtype instance FromJSONKey (PathTo k Abs Dir)
type Untag :: forall {k}. Base k -> Type
type family Untag pk where
Untag ('RelTo _) = P.Rel
Untag 'Abs = P.Abs
data Base k = RelTo k | Abs
type RelTo = 'RelTo
type Abs = 'Abs
(</>) :: PathTo parent b Dir -> PathTo child (RelTo parent) t -> PathTo child b t
{-# INLINE (</>) #-}
(</>) = coerce (P.</>)
infixr 5 </>
stripProperPrefix :: (MonadThrow m) => PathTo p b Dir -> PathTo l b t -> m (PathTo l (RelTo p) t)
stripProperPrefix (PathTo p) (PathTo l) = PathTo <$> P.stripProperPrefix p l
isProperPrefixOf :: PathTo p b Dir -> PathTo l b t -> Bool
isProperPrefixOf p l = isJust (stripProperPrefix p l)
replaceProperPrefix ::
(MonadThrow m) =>
PathTo parent b Dir ->
PathTo parent' b' Dir ->
PathTo child b t ->
m (PathTo child b' t)
replaceProperPrefix src dst fp = (dst </>) <$> stripProperPrefix (retagPath src) fp
replaceProperPrefix' ::
(MonadThrow m) =>
PathTo parent b Dir ->
PathTo parent b' Dir ->
PathTo child b t ->
m (PathTo child b' t)
replaceProperPrefix' src dst fp = (dst </>) <$> stripProperPrefix src fp
type ToSubpath :: forall {k}. Base k -> Base (Subpath k)
type family ToSubpath a where
ToSubpath 'Abs = 'Abs
ToSubpath ('RelTo e) = 'RelTo (Entry e)
parent :: PathTo e b t -> PathTo (ParentOf e) (ToSubpath b) Dir
parent = coerce P.parent
filename :: PathTo e b File -> PathTo (Entry e) (RelTo (ParentOf e)) File
filename = coerce P.filename
dirname :: PathTo e b Dir -> PathTo (Entry e) (RelTo (ParentOf e)) Dir
dirname = coerce P.dirname
addExtension :: forall e' m e b. (MonadThrow m) => String -> PathTo e b File -> m (PathTo e' b File)
addExtension ext = fmap PathTo . P.addExtension ext . coerce
splitExtension ::
forall e' m e b.
(MonadThrow m) =>
PathTo e b File ->
m (PathTo e' b File, String)
splitExtension = fmap (Bi.first PathTo) . P.splitExtension . coerce
fileExtension :: (MonadThrow m) => PathTo e b File -> m String
fileExtension = P.fileExtension . coerce
replaceExtension ::
forall e' m e b.
(MonadThrow m) =>
String ->
PathTo e b File ->
m (PathTo e' b File)
replaceExtension ext = fmap PathTo . P.replaceExtension ext . coerce
mapSomeBase :: (forall c. PathTo e c t -> PathTo e' c t') -> SomeBase e b t -> SomeBase e' b t'
mapSomeBase f = \case
IsAbs fp -> IsAbs (f fp)
IsRel fp -> IsRel (f fp)
prjSomeBase :: (forall c. PathTo e c t -> a) -> SomeBase e b t -> a
prjSomeBase f = \case
IsAbs fp -> f fp
IsRel fp -> f fp
parseAbsDir :: forall e m. (MonadThrow m) => FilePath -> m (PathTo e Abs Dir)
parseAbsDir = fmap coerce . P.parseAbsDir
parseRelDir :: forall e b m. (MonadThrow m) => FilePath -> m (PathTo e (RelTo b) Dir)
parseRelDir = fmap coerce . P.parseRelDir
parseAbsFile :: forall e m. (MonadThrow m) => FilePath -> m (PathTo e Abs File)
parseAbsFile = fmap coerce . P.parseAbsFile
parseRelFile :: forall e b m. (MonadThrow m) => FilePath -> m (PathTo e (RelTo b) File)
parseRelFile = fmap coerce . P.parseRelFile
parseSomeDir :: (MonadThrow m) => FilePath -> m (SomeBase e b Dir)
{-# INLINE parseSomeDir #-}
parseSomeDir fp =
P.parseSomeDir fp <&> \case
P.Abs p -> IsAbs $ PathTo p
P.Rel p -> IsRel $ PathTo p
parseSomeFile :: (MonadThrow m) => FilePath -> m (SomeBase e b File)
{-# INLINE parseSomeFile #-}
parseSomeFile fp =
P.parseSomeFile fp <&> \case
P.Abs p -> IsAbs $ PathTo p
P.Rel p -> IsRel $ PathTo p
data SomeBase e b t
= IsAbs (PathTo e Abs t)
| IsRel (PathTo e (RelTo b) t)
deriving (Show, Eq, Ord, Generic)
deriving anyclass (NFData, Hashable)
instance FromJSON (SomeBase e b Dir) where
parseJSON = parseJSONWith parseSomeDir
{-# INLINE parseJSON #-}
instance FromJSON (SomeBase e b File) where
parseJSON = parseJSONWith parseSomeFile
{-# INLINE parseJSON #-}
instance ToJSON (SomeBase e b t) where
toJSON = J.toJSON . prjSomeBase toFilePath
parseJSONWith ::
(Show e, FromJSON a) =>
(a -> Either e b) ->
Value ->
J.Parser b
parseJSONWith f x =
do
fp <- J.parseJSON x
case f fp of
Right p -> return p
Left e -> fail (show e)
{-# INLINE parseJSONWith #-}
untagSomeBase :: SomeBase e b t -> P.SomeBase t
untagSomeBase (IsAbs (PathTo fp)) = P.Abs fp
untagSomeBase (IsRel (PathTo fp)) = P.Rel fp
wrapE :: (FilePath -> ExpQ) -> FilePath -> ExpQ
wrapE e fp = [|PathTo $(e fp)|]
wrapPathQQ :: QuasiQuoter -> QuasiQuoter
wrapPathQQ qq = qq {quoteExp = wrapE (quoteExp qq)}
relfile :: QuasiQuoter
relfile = wrapPathQQ P.relfile
absfile :: QuasiQuoter
absfile = wrapPathQQ P.absfile
reldir :: QuasiQuoter
reldir = wrapPathQQ P.reldir
absdir :: QuasiQuoter
absdir = wrapPathQQ P.absdir
toFilePath :: PathTo e b t -> FilePath
toFilePath = coerce P.toFilePath
fromAbsDir :: PathTo e Abs Dir -> FilePath
fromAbsDir = P.fromAbsDir . untagPath
fromRelDir :: PathTo e (RelTo b) Dir -> FilePath
fromRelDir = P.fromRelDir . untagPath
fromAbsFile :: PathTo e Abs File -> FilePath
fromAbsFile = P.fromAbsFile . untagPath
fromRelFile :: PathTo e (RelTo b) File -> FilePath
fromRelFile = P.fromRelFile . untagPath
fromSomeDir :: SomeBase e b Dir -> FilePath
fromSomeDir = prjSomeBase toFilePath
fromSomeFile :: SomeBase e b File -> FilePath
fromSomeFile = prjSomeBase toFilePath
mkAbsDir :: FilePath -> ExpQ
mkAbsDir = wrapE mkAbsDir
mkRelDir :: FilePath -> ExpQ
mkRelDir = wrapE mkRelDir
mkAbsFile :: FilePath -> ExpQ
mkAbsFile = wrapE mkAbsFile
mkRelFile :: FilePath -> ExpQ
mkRelFile = wrapE mkRelFile
mkAbsDirT :: FilePath -> SpliceQ (PathTo e Abs Dir)
mkAbsDirT = unsafeSpliceCoerce . mkAbsDir
mkRelDirT :: FilePath -> SpliceQ (PathTo e (RelTo b) Dir)
mkRelDirT = unsafeSpliceCoerce . mkAbsDir
mkAbsFileT :: FilePath -> SpliceQ (PathTo e Abs File)
mkAbsFileT = unsafeSpliceCoerce . mkAbsFile
mkRelFileT :: FilePath -> SpliceQ (PathTo e (RelTo b) File)
mkRelFileT = unsafeSpliceCoerce . mkAbsFile