{- |
Module                  : Iris.Tool
Copyright               : (c) 2022 Dmitrii Kovanikov
SPDX-License-Identifier : MPL-2.0
Maintainer              : Dmitrii Kovanikov <kovanikov@gmail.com>
Stability               : Experimental
Portability             : Portable

Utilities to check required tools and their minimal version for a CLI app.

Sometimes, your CLI application

@since 0.0.0.0
-}
module Iris.Tool (
    -- * Requiring an executable
    need,
    Tool (..),
    ToolSelector (..),
    defaultToolSelector,

    -- * Tool requirements check
    ToolCheckResult (..),
    ToolCheckError (..),
    ToolCheckException (..),
    checkTool,
) where

import Control.Exception (Exception, throwIO)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Foldable (traverse_)
import Data.String (IsString (..))
import Data.Text (Text)
import System.Directory (findExecutable)
import System.Process (readProcess)

import qualified Data.Text as Text

{- |

@since 0.0.0.0
-}
data Tool = Tool
    { Tool -> Text
toolName :: Text
    -- ^ @since 0.0.0.0
    , Tool -> Maybe ToolSelector
toolSelector :: Maybe ToolSelector
    -- ^ @since 0.0.0.0
    }

{- |

@since 0.0.0.0
-}
instance IsString Tool where
    fromString :: String -> Tool
    fromString :: String -> Tool
fromString String
s =
        Tool
            { toolName :: Text
toolName = forall a. IsString a => String -> a
fromString String
s
            , toolSelector :: Maybe ToolSelector
toolSelector = forall a. Maybe a
Nothing
            }

{- |

@since 0.0.0.0
-}
data ToolSelector = ToolSelector
    { ToolSelector -> Text -> Bool
toolSelectorFunction :: Text -> Bool
    -- ^ @since 0.0.0.0
    , ToolSelector -> Maybe Text
toolSelectorVersionArg :: Maybe Text
    -- ^ @since 0.0.0.0
    }

{- |

@since 0.0.0.0
-}
defaultToolSelector :: ToolSelector
defaultToolSelector :: ToolSelector
defaultToolSelector =
    ToolSelector
        { toolSelectorFunction :: Text -> Bool
toolSelectorFunction = forall a b. a -> b -> a
const Bool
True
        , toolSelectorVersionArg :: Maybe Text
toolSelectorVersionArg = forall a. Maybe a
Nothing
        }

{- |

@since 0.0.0.0
-}
data ToolCheckResult
    = -- |
      --
      --     @since 0.1.0.0
      ToolCheckError ToolCheckError
    | -- |
      --
      --     @since 0.0.0.0
      ToolOk
    deriving stock
        ( Int -> ToolCheckResult -> ShowS
[ToolCheckResult] -> ShowS
ToolCheckResult -> String
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
showList :: [ToolCheckResult] -> ShowS
$cshowList :: [ToolCheckResult] -> ShowS
show :: ToolCheckResult -> String
$cshow :: ToolCheckResult -> String
showsPrec :: Int -> ToolCheckResult -> ShowS
$cshowsPrec :: Int -> ToolCheckResult -> ShowS
Show
          -- ^ @since 0.0.0.0
        , ToolCheckResult -> ToolCheckResult -> Bool
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
/= :: ToolCheckResult -> ToolCheckResult -> Bool
$c/= :: ToolCheckResult -> ToolCheckResult -> Bool
== :: ToolCheckResult -> ToolCheckResult -> Bool
$c== :: ToolCheckResult -> ToolCheckResult -> Bool
Eq
          -- ^ @since 0.0.0.0
        )

{- |

@since 0.1.0.0
-}
data ToolCheckError
    = -- |
      --
      --     @since 0.1.0.0
      ToolNotFound Text
    | -- |
      --
      --     @since 0.1.0.0
      ToolWrongVersion Text
    deriving stock
        ( Int -> ToolCheckError -> ShowS
[ToolCheckError] -> ShowS
ToolCheckError -> String
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
showList :: [ToolCheckError] -> ShowS
$cshowList :: [ToolCheckError] -> ShowS
show :: ToolCheckError -> String
$cshow :: ToolCheckError -> String
showsPrec :: Int -> ToolCheckError -> ShowS
$cshowsPrec :: Int -> ToolCheckError -> ShowS
Show
          -- ^ @since 0.1.0.0
        , ToolCheckError -> ToolCheckError -> Bool
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
/= :: ToolCheckError -> ToolCheckError -> Bool
$c/= :: ToolCheckError -> ToolCheckError -> Bool
== :: ToolCheckError -> ToolCheckError -> Bool
$c== :: ToolCheckError -> ToolCheckError -> Bool
Eq
          -- ^ @since 0.1.0.0
        )

{- |

@since 0.0.0.0
-}
checkTool :: Tool -> IO ToolCheckResult
checkTool :: Tool -> IO ToolCheckResult
checkTool Tool{Maybe ToolSelector
Text
toolSelector :: Maybe ToolSelector
toolName :: Text
toolSelector :: Tool -> Maybe ToolSelector
toolName :: Tool -> Text
..} =
    String -> IO (Maybe String)
findExecutable (Text -> String
Text.unpack Text
toolName) forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= \case
        Maybe String
Nothing -> forall (f :: * -> *) a. Applicative f => a -> f a
pure forall a b. (a -> b) -> a -> b
$ ToolCheckError -> ToolCheckResult
ToolCheckError forall a b. (a -> b) -> a -> b
$ Text -> ToolCheckError
ToolNotFound Text
toolName
        Just String
exe -> case Maybe ToolSelector
toolSelector of
            Maybe ToolSelector
Nothing -> forall (f :: * -> *) a. Applicative f => a -> f a
pure ToolCheckResult
ToolOk
            Just ToolSelector{Maybe Text
Text -> Bool
toolSelectorVersionArg :: Maybe Text
toolSelectorFunction :: Text -> Bool
toolSelectorVersionArg :: ToolSelector -> Maybe Text
toolSelectorFunction :: ToolSelector -> Text -> Bool
..} -> case Maybe Text
toolSelectorVersionArg of
                Maybe Text
Nothing -> forall (f :: * -> *) a. Applicative f => a -> f a
pure ToolCheckResult
ToolOk
                Just Text
versionArg -> do
                    String
toolVersionOutput <- String -> [String] -> String -> IO String
readProcess String
exe [Text -> String
Text.unpack Text
versionArg] String
""
                    let version :: Text
version = Text -> Text
Text.strip forall a b. (a -> b) -> a -> b
$ String -> Text
Text.pack String
toolVersionOutput

                    if Text -> Bool
toolSelectorFunction Text
version
                        then forall (f :: * -> *) a. Applicative f => a -> f a
pure ToolCheckResult
ToolOk
                        else forall (f :: * -> *) a. Applicative f => a -> f a
pure forall a b. (a -> b) -> a -> b
$ ToolCheckError -> ToolCheckResult
ToolCheckError forall a b. (a -> b) -> a -> b
$ Text -> ToolCheckError
ToolWrongVersion Text
version

{- | An exception thrown by 'need' when there's an error requiring a tool.

@since 0.1.0.0
-}
newtype ToolCheckException = ToolCheckException ToolCheckError
    deriving stock
        ( Int -> ToolCheckException -> ShowS
[ToolCheckException] -> ShowS
ToolCheckException -> String
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
showList :: [ToolCheckException] -> ShowS
$cshowList :: [ToolCheckException] -> ShowS
show :: ToolCheckException -> String
$cshow :: ToolCheckException -> String
showsPrec :: Int -> ToolCheckException -> ShowS
$cshowsPrec :: Int -> ToolCheckException -> ShowS
Show
          -- ^ @since 0.1.0.0
        )
    deriving newtype
        ( ToolCheckException -> ToolCheckException -> Bool
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
/= :: ToolCheckException -> ToolCheckException -> Bool
$c/= :: ToolCheckException -> ToolCheckException -> Bool
== :: ToolCheckException -> ToolCheckException -> Bool
$c== :: ToolCheckException -> ToolCheckException -> Bool
Eq
          -- ^ @since 0.1.0.0
        )
    deriving anyclass
        ( Show ToolCheckException
Typeable ToolCheckException
SomeException -> Maybe ToolCheckException
ToolCheckException -> String
ToolCheckException -> SomeException
forall e.
Typeable e
-> Show e
-> (e -> SomeException)
-> (SomeException -> Maybe e)
-> (e -> String)
-> Exception e
displayException :: ToolCheckException -> String
$cdisplayException :: ToolCheckException -> String
fromException :: SomeException -> Maybe ToolCheckException
$cfromException :: SomeException -> Maybe ToolCheckException
toException :: ToolCheckException -> SomeException
$ctoException :: ToolCheckException -> SomeException
Exception
          -- ^ @since 0.1.0.0
        )

{- | Use this function to require specific CLI tools for your CLI application.

The function can be used in the beginning of each command in the following way:

@
app :: App ()
app = Iris.'Iris.Env.asksCliEnv' Iris.'Iris.Env.cliEnvCmd' >>= __\\case__
    Download url -> do
        Iris.'need' ["curl"]
        runDownload url
    Evaluate hs -> do
        Iris.'need' ["ghc", "cabal"]
        runEvaluate hs
@

__Throws:__ 'ToolCheckException' if can't find a tool or if it has wrong version.

@since 0.0.0.0
-}
need :: MonadIO m => [Tool] -> m ()
need :: forall (m :: * -> *). MonadIO m => [Tool] -> m ()
need = forall (t :: * -> *) (f :: * -> *) a b.
(Foldable t, Applicative f) =>
(a -> f b) -> t a -> f ()
traverse_ forall a b. (a -> b) -> a -> b
$ \Tool
tool ->
    forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO forall a b. (a -> b) -> a -> b
$
        Tool -> IO ToolCheckResult
checkTool Tool
tool forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= \case
            ToolCheckResult
ToolOk -> forall (f :: * -> *) a. Applicative f => a -> f a
pure ()
            ToolCheckError ToolCheckError
toolErr -> forall e a. Exception e => e -> IO a
throwIO forall a b. (a -> b) -> a -> b
$ ToolCheckError -> ToolCheckException
ToolCheckException ToolCheckError
toolErr