module System.REPL.Ask (
PromptMsg,
TypeError,
PredicateError,
Predicate,
Predicate',
Parser,
Asker(..),
Asker',
SomeREPLError(..),
SomeAskerError(..),
AskerTypeError(..),
AskerPredicateError(..),
GenericTypeError(..),
GenericPredicateError(..),
genericTypeError,
genericPredicateError,
typeAskerP,
maybeAskerP,
Verbatim(..),
readParser,
asker,
lineAsker,
typeAsker,
predAsker,
maybeAsker,
ask,
ask',
askEither,
untilValid,
boolPredicate,
PathRootDoesNotExist(..),
PathIsNotWritable(..),
filepathAsker,
writablefilepathAsker,
) where
import Prelude hiding (putStrLn, putStr, getLine, reverse)
import Control.Arrow (right, (|||))
import Control.Monad.Catch
import Control.Monad.IO.Class (MonadIO(liftIO))
import Data.Char (isSpace)
import Data.Functor.Monadic
import qualified Data.List as L
import qualified Data.Text as T
import qualified System.Directory as D
import qualified System.FilePath as FP
import qualified System.IO.Error as ERR
import System.REPL.Prompt
import System.REPL.Types
import Text.Read (readMaybe)
typeAskerP :: Applicative m
=> PromptMsg
-> Parser a
-> Asker' m a
typeAskerP pr parse = Asker pr parse (pure . Right)
maybeAskerP :: Applicative m
=> PromptMsg
-> Parser a
-> Predicate m a b
-> Asker m (Maybe a) (Maybe b)
maybeAskerP pr parse pred = Asker pr parse' check
where
parse' t = if T.all isSpace t then Right Nothing
else right Just $ parse t
check Nothing = pure $ Right Nothing
check (Just t) = pred t >$> (\case Right t -> Right (Just t)
Left err -> Left err)
readParser :: Read a
=> (T.Text -> TypeError)
-> Parser a
readParser errT t = maybe (Left $ errT t) Right . readMaybe . T.unpack $ t
asker :: (Functor m, Read a)
=> PromptMsg
-> (T.Text -> TypeError)
-> Predicate' m a
-> Asker' m a
asker pr errT pred = Asker pr (readParser errT) pred
typeAsker :: (Applicative m, Read a)
=> PromptMsg
-> (T.Text -> TypeError)
-> Asker' m a
typeAsker p errT = asker p errT (pure . Right)
predAsker :: (Functor m)
=> PromptMsg
-> Predicate m T.Text b
-> Asker m T.Text b
predAsker pr f = Asker pr Right f
lineAsker :: Applicative m
=> Asker' m T.Text
lineAsker = predAsker "" (pure . Right)
maybeAsker :: (Applicative m, Read a)
=> PromptMsg
-> (T.Text -> TypeError)
-> Predicate' m a
-> Asker' m (Maybe a)
maybeAsker pr errT pred = maybeAskerP pr (readParser errT) pred
ask :: (MonadIO m, MonadCatch m)
=> Asker m a b
-> Maybe T.Text
-> m b
ask a v = askEither a v >>= either throwM return
ask' :: (MonadIO m, MonadCatch m)
=> Asker m a b
-> m b
ask' a = ask a Nothing
askEither :: (MonadIO m, MonadCatch m)
=> Asker m a b
-> Maybe T.Text
-> m (Either SomeAskerError b)
askEither a = maybe getInput check
where
getInput = (promptAbort '\ESC' (askerPrompt a) >>= check)
`catch` (return . Left)
check inp = case askerParser a inp of
Left err -> return . Left . SomeAskerError . AskerTypeError $ err
Right t -> askerPredicate a t
>>= return . (Left . SomeAskerError . AskerPredicateError ||| Right)
untilValid :: forall m a.(MonadIO m, MonadCatch m, Read a)
=> m a
-> m a
untilValid m = m `catch` handler
where
handler :: SomeAskerError -> m a
handler l = liftIO (putStrLn $ show l) >> untilValid m
boolPredicate :: Functor m
=> (a -> m Bool)
-> (a -> PredicateError)
-> Predicate' m a
boolPredicate f errP t = (\case {True -> Right t; False -> Left (errP t)}) <$> f t
filepathAsker :: MonadIO m
=> PromptMsg
-> (FilePath -> TypeError)
-> Predicate m (PathExistenceType, FilePath) b
-> Asker m FilePath b
filepathAsker pr errT pred = Asker pr parse pred'
where
parse = (\fp -> if FP.isValid fp then Right fp else Left $ errT fp) . T.unpack
pred' fp = do
exType <- liftIO $ getExistenceType fp
pred (exType, fp)
getExistenceType :: FilePath -> IO PathExistenceType
getExistenceType fp = do
isDir <- D.doesDirectoryExist fp
if isDir then return IsDirectory
else do isFile <- D.doesFileExist fp
return $ if isFile then IsFile
else DoesNotExist
writablefilepathAsker
:: MonadIO m
=> PromptMsg
-> (FilePath -> TypeError)
-> Predicate m (PathExistenceType, FilePath) b
-> Asker m FilePath b
writablefilepathAsker pr errT pred = filepathAsker pr errT pred'
where
permError e = if ERR.isPermissionErrorType (ERR.ioeGetErrorType e) ||
ERR.isDoesNotExistErrorType (ERR.ioeGetErrorType e)
then Just () else Nothing
conc :: [FilePath] -> FilePath
conc = L.foldl' (FP.</>) ""
doesExist fp = (||) <$> D.doesDirectoryExist (conc fp) <*> D.doesFileExist (conc fp)
isWritable fp = catchJust permError (fp >>= D.getPermissions >$> D.writable) (const $ return False)
boolEither :: (Monad m, Exception a) => (m Bool) -> a -> m (Either SomeException b) -> m (Either SomeException b)
boolEither x falseCase trueCase = x >>= (\case{True -> trueCase; False -> return $ Left $ SomeException falseCase})
pred' args@(_, fp) =
if FP.isRelative fp then boolEither (liftIO $ isWritable D.getCurrentDirectory) (PathIsNotWritable fp) (pred args)
else do
existingRoot <- liftIO $ takeWhile snd <$> mapM (\x -> (x,) <$> doesExist x) (L.inits $ FP.splitDirectories fp)
if null existingRoot then return (Left $ SomeException $ PathRootDoesNotExist fp)
else boolEither (liftIO $ isWritable (return . conc . fst . last $ existingRoot)) (PathIsNotWritable fp) (pred args)