packages feed

iris-0.1.0.0: src/Iris/Tool.hs

{- |
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
    { toolName :: Text
    -- ^ @since 0.0.0.0
    , toolSelector :: Maybe ToolSelector
    -- ^ @since 0.0.0.0
    }

{- |

@since 0.0.0.0
-}
instance IsString Tool where
    fromString :: String -> Tool
    fromString s =
        Tool
            { toolName = fromString s
            , toolSelector = Nothing
            }

{- |

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

{- |

@since 0.0.0.0
-}
defaultToolSelector :: ToolSelector
defaultToolSelector =
    ToolSelector
        { toolSelectorFunction = const True
        , toolSelectorVersionArg = Nothing
        }

{- |

@since 0.0.0.0
-}
data ToolCheckResult
    = -- |
      --
      --     @since 0.1.0.0
      ToolCheckError ToolCheckError
    | -- |
      --
      --     @since 0.0.0.0
      ToolOk
    deriving stock
        ( Show
          -- ^ @since 0.0.0.0
        , 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
        ( Show
          -- ^ @since 0.1.0.0
        , Eq
          -- ^ @since 0.1.0.0
        )

{- |

@since 0.0.0.0
-}
checkTool :: Tool -> IO ToolCheckResult
checkTool Tool{..} =
    findExecutable (Text.unpack toolName) >>= \case
        Nothing -> pure $ ToolCheckError $ ToolNotFound toolName
        Just exe -> case toolSelector of
            Nothing -> pure ToolOk
            Just ToolSelector{..} -> case toolSelectorVersionArg of
                Nothing -> pure ToolOk
                Just versionArg -> do
                    toolVersionOutput <- readProcess exe [Text.unpack versionArg] ""
                    let version = Text.strip $ Text.pack toolVersionOutput

                    if toolSelectorFunction version
                        then pure ToolOk
                        else pure $ ToolCheckError $ ToolWrongVersion 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
        ( Show
          -- ^ @since 0.1.0.0
        )
    deriving newtype
        ( Eq
          -- ^ @since 0.1.0.0
        )
    deriving anyclass
        ( 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 = traverse_ $ \tool ->
    liftIO $
        checkTool tool >>= \case
            ToolOk -> pure ()
            ToolCheckError toolErr -> throwIO $ ToolCheckException toolErr