packages feed

elynx-tools-0.2.1: src/ELynx/Tools/InputOutput.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell   #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

{- |
Module      :  ELynx.Tools.InputOutput
Copyright   :  (c) Dominik Schrempf 2020
License     :  GPL-3.0-or-later

Maintainer  :  dominik.schrempf@gmail.com
Stability   :  unstable
Portability :  portable

Creation date: Thu Feb 14 13:30:37 2019.

Tools involving input, output, and parsing.

-}

module ELynx.Tools.InputOutput
  ( -- * Input, output
    getOutFilePath
  , openFile'
  , readGZFile
  , writeGZFile
  , out
  , outHandle
    -- * Parsing
  , runParserOnFile
  , parseFileWith
  , parseIOWith
  , parseFileOrIOWith
  , parseStringWith
  , parseByteStringWith
  )
where

import           Codec.Compression.GZip         ( compress
                                                , decompress
                                                )
import           Control.Monad.Trans.Reader     ( ask )
import           Control.DeepSeq                ( force )
import           Control.Exception              ( evaluate )
import           Control.Monad                  ( (<=<) )
import           Control.Monad.IO.Class
import           Control.Monad.Logger
import qualified Data.ByteString.Lazy.Char8    as L
import           Data.List                      ( isSuffixOf )
import           Data.Maybe
import qualified Data.Text                     as T
import           System.IO
import           System.Directory               ( doesFileExist )
import           Text.Megaparsec

import           ELynx.Tools.Reproduction       ( ELynx
                                                , Force(..)
                                                , Arguments(..)
                                                , outFileBaseName
                                                , forceReanalysis
                                                , Reproducible(..)
                                                )

-- | Get out file path with extension.
getOutFilePath
  :: forall a . Reproducible a => String -> ELynx a (Maybe FilePath)
getOutFilePath ext = do
  a <- ask
  let bn   = outFileBaseName . global $ a
      sfxs = outSuffixes a
  if ext `elem` sfxs
    then return $ (++ ext) <$> bn
    else
      error
        "getOutFilePath: out file suffix not registered. Please contact maintainer."

checkFile :: Force -> FilePath -> IO ()
checkFile (Force True ) _  = return ()
checkFile (Force False) fp = doesFileExist fp >>= \case
  True ->
    error
      $  "File exists: "
      <> fp
      <> ". Please use the --force option to repeat an analysis."
  False -> return ()

-- | Open existing files only if 'Force' is true.
openFile' :: Force -> FilePath -> IOMode -> IO Handle
openFile' frc fp md = checkFile frc fp >> openFile fp md

-- XXX: For now, all files are read strictly (see help of
-- Control.DeepSeq.force).
readFile' :: FilePath -> IO L.ByteString
readFile' fn = withFile fn ReadMode $ (evaluate . force) <=< L.hGetContents

-- | Read file. If file path ends with ".gz", assume gzipped file and decompress
-- before read.
readGZFile :: FilePath -> IO L.ByteString
readGZFile f | ".gz" `isSuffixOf` f = decompress <$> readFile' f
             | otherwise            = readFile' f

-- | Write file. If file path ends with ".gz", assume gzipped file and compress
-- before write.
writeGZFile :: Force -> FilePath -> L.ByteString -> IO ()
writeGZFile frc f r
  | ".gz" `isSuffixOf` f = checkFile frc f >> L.writeFile f (compress r)
  | otherwise            = checkFile frc f >> L.writeFile f r

-- | Parse a possibly gzipped file.
runParserOnFile
  :: Parsec e L.ByteString a
  -> FilePath
  -> IO (Either (ParseErrorBundle L.ByteString e) a)
runParserOnFile p f = parse p f <$> readGZFile f

-- | Parse a possibly gzipped file and extract the result.
parseFileWith
  :: (ShowErrorComponent e)
  => Parsec e L.ByteString a -- ^ The parser.
  -> FilePath
  -> IO a
parseFileWith p f = parseFileOrIOWith p (Just f)

-- | Parse standard input.
parseIOWith
  :: (ShowErrorComponent e)
  => Parsec e L.ByteString a -- ^ The parser.
  -> IO a
parseIOWith p = parseByteStringWith "Standard input" p <$> L.getContents

-- | Parse a possibly gzipped file, or standard input, and extract the result.
parseFileOrIOWith
  :: (ShowErrorComponent e)
  => Parsec e L.ByteString a -- ^ The parser.
  -> Maybe FilePath          -- ^ If no file path is given, standard input is used.
  -> IO a
parseFileOrIOWith p mf = do
  contents <- maybe L.getContents readGZFile mf
  return $ parseByteStringWith (fromMaybe "Standard input" mf) p contents

-- | Parse a 'String' and extract the result.
parseStringWith
  :: (ShowErrorComponent e)
  => String                  -- ^ Name of string.
  -> Parsec e L.ByteString a -- ^ Parser.
  -> String                  -- ^ Input.
  -> a
parseStringWith s p l = parseByteStringWith s p (L.pack l)

-- | Parse a 'L.ByteString' and extract the result.
parseByteStringWith
  :: (ShowErrorComponent e)
  => String                  -- ^ Name of byte string.
  -> Parsec e L.ByteString a -- ^ Parser.
  -> L.ByteString            -- ^ Input.
  -> a
parseByteStringWith s p l = case parse p s l of
  Left  err -> error $ errorBundlePretty err
  Right val -> val

-- | Write a result with a given name to file with given extension or standard
-- output. Supports compression.
out :: Reproducible a => String -> L.ByteString -> String -> ELynx a ()
out name res ext = do
  mfp <- getOutFilePath ext
  case mfp of
    Nothing -> do
      $(logInfo) $ T.pack $ "Write " <> name <> " to standard output."
      liftIO $ L.putStr res
    Just fp -> do
      $(logInfo) $ T.pack $ "Write " <> name <> " to file '" <> fp <> "'."
      frc <- forceReanalysis . global <$> ask
      liftIO $ writeGZFile frc fp res

-- | Get an output handle, does not support compression. The handle has to be
-- closed after use!
outHandle :: Reproducible a => String -> String -> ELynx a Handle
outHandle name ext = do
  mfp <- getOutFilePath ext
  case mfp of
    Nothing -> do
      $(logInfo) $ T.pack $ "Write " <> name <> " to standard output."
      return stdout
    Just fp -> do
      $(logInfo) $ T.pack $ "Write " <> name <> " to file '" <> fp <> "'."
      frc <- forceReanalysis . global <$> ask
      liftIO $ openFile' frc fp WriteMode