packages feed

glean-lsp-0.1.0.0: Data/Path.hs

{-
  Copyright (c) 2023 Joseph Sumabat

  See glean/lsp/LICENSE
-}

module Data.Path (
  Path (path, Path, UncheckedPath),
  KnownPathKind (sPathKind),
  AbsPath,
  PathKind (..),
  RelPath,
  filePathToAbs,
  unsafeFilePathToAbs,
  filePathToRel,
  toFilePath,
  (</>),
  makeRelative,
  absToRel,
  (<.>),
  (-<.>),
  filePathToAbsThrow,
  uncheckedCoercePath,
  relToAbsThrow,
)
where

import Control.Monad.Catch
import Control.Monad.IO.Class
import Data.Aeson qualified as Aeson
import Data.Coerce (coerce)
import Data.Hashable (Hashable)
import Data.String (IsString (..))
import GHC.Stack (HasCallStack)
import System.Directory qualified as Dir
import System.FilePath qualified as FilePath
import UnliftIO (stringException)

-- | Rel means may be relative or absolute, absolute means must be absolute
data PathKind = Rel | Abs

data SPathKind p where
  SRel :: SPathKind Rel
  SAbs :: SPathKind Abs

class KnownPathKind p where
  sPathKind :: SPathKind p

instance KnownPathKind Abs where
  sPathKind = SAbs

instance KnownPathKind Rel where
  sPathKind = SRel

newtype Path (p :: PathKind) = UncheckedPath {path :: FilePath}
  deriving (Show, Eq, Ord, Hashable, Aeson.FromJSON, Aeson.ToJSON)

uncheckedCoercePath :: Path p -> Path q
uncheckedCoercePath = coerce

instance IsString (Path Rel) where
  fromString = UncheckedPath

pattern Path :: FilePath -> Path p
pattern Path p <- UncheckedPath p

type AbsPath = Path Abs

type RelPath = Path Rel

toFilePath :: (HasCallStack) => Path p -> FilePath
toFilePath = (.path)

filePathToRel :: (HasCallStack) => FilePath -> RelPath
filePathToRel = UncheckedPath

filePathToAbs :: (HasCallStack, MonadIO m) => FilePath -> m AbsPath
filePathToAbs p = do
  absPath <- liftIO $ Dir.makeAbsolute p
  pure $ UncheckedPath absPath

unsafeFilePathToAbs :: (HasCallStack) => FilePath -> AbsPath
unsafeFilePathToAbs p
  | FilePath.isAbsolute p = UncheckedPath p
  | otherwise = error "unsafeOsPathToAbs: path is not absolute"

relToAbsThrow :: (MonadThrow m, HasCallStack) => RelPath -> m AbsPath
relToAbsThrow (UncheckedPath p) = filePathToAbsThrow p

filePathToAbsThrow :: (MonadThrow m, HasCallStack) => FilePath -> m AbsPath
filePathToAbsThrow p
  | FilePath.isAbsolute p = pure $ UncheckedPath p
  | otherwise = throwM (stringException $ "filepath was not absolute: " ++ p)

(</>) :: Path p -> Path Rel -> Path p
(UncheckedPath p) </> (UncheckedPath p') = UncheckedPath (p FilePath.</> p')

infixr 5 </>

(<.>) :: Path p -> String -> Path p
(UncheckedPath p) <.> ext = UncheckedPath (p FilePath.<.> ext)

(-<.>) :: Path p -> String -> Path p
(UncheckedPath p) -<.> ext = UncheckedPath (p FilePath.-<.> ext)

absToRel :: AbsPath -> RelPath
absToRel (UncheckedPath p) = UncheckedPath p

makeRelative :: Path p -> Path q -> Path Rel
makeRelative (UncheckedPath p) (UncheckedPath q) = UncheckedPath (FilePath.makeRelative p q)