hsdev-0.1.4.0: src/HsDev/Util.hs
{-# LANGUAGE FlexibleContexts #-}
module HsDev.Util (
withCurrentDirectory,
directoryContents,
traverseDirectory, searchPath,
isParent,
haskellSource,
cabalFile,
-- * String utils
tab, tabs, trim, split,
-- * Other utils
ordNub,
-- * Helper
(.::), (.::?), objectUnion,
-- * Exceptions
liftException, liftE, liftEIO, tries, triesMap, liftExceptionM, liftIOErrors,
eitherT,
liftThrow,
-- * UTF-8
fromUtf8, toUtf8, readFileUtf8, writeFileUtf8,
-- * IO
hGetLineBS, logException, logIO, ignoreIO,
-- * Task
liftTask,
-- * Reexportss
module Control.Monad.Except,
MonadIO(..)
) where
import Control.Arrow (second, left)
import Control.Exception
import Control.Monad
import Control.Monad.Except
import qualified Control.Monad.Catch as C
import Data.Aeson
import Data.Aeson.Types (Parser)
import Data.Char (isSpace)
import Data.List (isPrefixOf, unfoldr)
import Data.Maybe (catMaybes)
import qualified Data.Set as Set
import qualified Data.HashMap.Strict as HM (HashMap, toList, union)
import qualified Data.ByteString.Char8 as B
import Data.ByteString.Lazy (ByteString)
import qualified Data.ByteString.Lazy.Char8 as L
import Data.Text (Text)
import qualified Data.Text.Lazy as T
import qualified Data.Text.Lazy.Encoding as T
import System.Directory
import System.FilePath
import System.IO
import Control.Concurrent.Task
-- | Run action with current directory set
withCurrentDirectory :: FilePath -> IO a -> IO a
withCurrentDirectory cur act = bracket getCurrentDirectory setCurrentDirectory $
const (setCurrentDirectory cur >> act)
-- | Get directory contents safely
directoryContents :: FilePath -> IO [FilePath]
directoryContents p = handle ignore $ do
b <- doesDirectoryExist p
if b
then liftM (map (p </>) . filter (`notElem` [".", ".."])) (getDirectoryContents p)
else return []
where
ignore :: SomeException -> IO [FilePath]
ignore _ = return []
-- | Collect all file names in directory recursively
traverseDirectory :: FilePath -> IO [FilePath]
traverseDirectory path = handle onError $ do
cts <- directoryContents path
liftM concat $ forM cts $ \c -> do
isDir <- doesDirectoryExist c
if isDir
then (c :) <$> traverseDirectory c
else return [c]
where
onError :: IOException -> IO [FilePath]
onError _ = return []
-- | Search something up
searchPath :: (MonadIO m, MonadPlus m) => FilePath -> (FilePath -> m a) -> m a
searchPath path f = do
path' <- liftIO $ canonicalizePath path
isDir <- liftIO $ doesDirectoryExist path'
search' (if isDir then path' else takeDirectory path')
where
search' dir
| isDrive dir = f dir
| otherwise = f dir `mplus` search' (takeDirectory dir)
-- | Is one path parent of another
isParent :: FilePath -> FilePath -> Bool
isParent dir file = norm dir `isPrefixOf` norm file where
norm = splitDirectories . normalise
-- | Is haskell source?
haskellSource :: FilePath -> Bool
haskellSource f = takeExtension f `elem` [".hs", ".lhs"]
-- | Is cabal file?
cabalFile :: FilePath -> Bool
cabalFile f = takeExtension f == ".cabal"
-- | Add N tabs to line
tab :: Int -> String -> String
tab n s = replicate n '\t' ++ s
-- | Add N tabs to multiline
tabs :: Int -> String -> String
tabs n = unlines . map (tab n) . lines
-- | Trim string
trim :: String -> String
trim = p . p where
p = reverse . dropWhile isSpace
-- | Split list
split :: (a -> Bool) -> [a] -> [[a]]
split p = takeWhile (not . null) . unfoldr (Just . second (drop 1) . break p)
-- | ordNub is quadratic, https://github.com/nh2/haskell-ordnub/#ordnub
ordNub :: Ord a => [a] -> [a]
ordNub l = go Set.empty l where
go _ [] = []
go s (x:xs)
| x `Set.member` s = go s xs
| otherwise = x : go (Set.insert x s) xs
-- | Workaround, sometimes we get HM.lookup "foo" v == Nothing, but lookup "foo" (HM.toList v) == Just smth
(.::) :: FromJSON a => HM.HashMap Text Value -> Text -> Parser a
v .:: name = maybe (fail $ "key " ++ show name ++ " not present") parseJSON $ lookup name $ HM.toList v
(.::?) :: FromJSON a => HM.HashMap Text Value -> Text -> Parser (Maybe a)
v .::? name = traverse parseJSON $ lookup name $ HM.toList v
-- | Union two JSON objects
objectUnion :: Value -> Value -> Value
objectUnion (Object l) (Object r) = Object $ HM.union l r
objectUnion (Object l) _ = Object l
objectUnion _ (Object r) = Object r
objectUnion _ _ = Null
-- | Lift IO exception to ExceptT
liftException :: C.MonadCatch m => m a -> ExceptT String m a
liftException = ExceptT . liftM (left $ \(SomeException e) -> show e) . C.try
-- | Same as @liftException@
liftE :: C.MonadCatch m => m a -> ExceptT String m a
liftE = liftException
-- | @liftE@ for IO
liftEIO :: (C.MonadCatch m, MonadIO m) => IO a -> ExceptT String m a
liftEIO = liftE . liftIO
-- | Run actions ignoring errors
tries :: MonadPlus m => [m a] -> m [a]
tries acts = liftM catMaybes $ sequence [liftM Just act `mplus` return Nothing | act <- acts]
triesMap :: MonadPlus m => (a -> m b) -> [a] -> m [b]
triesMap f = tries . map f
-- | Lift IO exception to MonadError
liftExceptionM :: (C.MonadCatch m, MonadError String m) => m a -> m a
liftExceptionM act = C.catch act onError where
onError = throwError . (\(SomeException e) -> show e)
-- | Lift IO exceptions to ExceptT
liftIOErrors :: C.MonadCatch m => ExceptT String m a -> ExceptT String m a
liftIOErrors act = liftException (runExceptT act) >>= either throwError return
eitherT :: (Monad m, MonadError String m) => Either String a -> m a
eitherT = either throwError return
-- | Throw error as exception
liftThrow :: (Show e, MonadError e m, C.MonadCatch m) => m a -> m a
liftThrow act = catchError act (C.throwM . userError . show)
fromUtf8 :: ByteString -> String
fromUtf8 = T.unpack . T.decodeUtf8
toUtf8 :: String -> ByteString
toUtf8 = T.encodeUtf8 . T.pack
-- | Read file in UTF8
readFileUtf8 :: FilePath -> IO String
readFileUtf8 f = withFile f ReadMode $ \h -> do
hSetEncoding h utf8
cts <- hGetContents h
length cts `seq` return cts
writeFileUtf8 :: FilePath -> String -> IO ()
writeFileUtf8 f cts = withFile f WriteMode $ \h -> do
hSetEncoding h utf8
hPutStr h cts
hGetLineBS :: Handle -> IO ByteString
hGetLineBS = fmap L.fromStrict . B.hGetLine
logException :: String -> (String -> IO ()) -> IO () -> IO ()
logException pre out = handle onErr where
onErr :: SomeException -> IO ()
onErr e = out $ pre ++ show e
logIO :: String -> (String -> IO ()) -> IO () -> IO ()
logIO pre out = handle onIO where
onIO :: IOException -> IO ()
onIO e = out $ pre ++ show e
ignoreIO :: IO () -> IO ()
ignoreIO = handle (const (return ()) :: IOException -> IO ())
liftTask :: (C.MonadThrow m, C.MonadCatch m, MonadIO m) => IO (Task a) -> ExceptT String m a
liftTask = liftExceptionM . ExceptT . liftIO . liftM (left show) . join . liftM taskWait