{-# LANGUAGE LambdaCase #-}
-- | Those are utility functions, most of them being pretty much self
-- explanatory.
module Puppet.Utils (
textElem
, getDirectoryContents
, takeBaseName
, takeDirectory
, strictifyEither
, loadYamlFile
, scientific2text
, text2Scientific
, getFiles
, ifromList, ikeys, isingleton, ifromListWith, iunionWith, iinsertWith
-- * re-export
, module Data.Monoid
) where
import Data.Attoparsec.Text (parseOnly, rational)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.ByteString as BS
import qualified Data.Foldable as F
import Data.Hashable
import qualified Data.HashMap.Strict as HM
import qualified Data.HashSet as HS
import Data.Monoid
import System.Posix.Directory.ByteString
import qualified Data.Either.Strict as S
import Data.Scientific
import Control.Exception
import Control.Lens
import Data.Aeson.Lens
import qualified Data.Yaml as Y
text2Scientific :: T.Text -> Maybe Scientific
text2Scientific t = case parseOnly rational t of
Left _ -> Nothing
Right s -> Just s
scientific2text :: Scientific -> T.Text
scientific2text n = T.pack $ case n ^? _Integer of
Just i -> show i
_ -> show n
strictifyEither :: Either a b -> S.Either a b
strictifyEither (Left x) = S.Left x
strictifyEither (Right x) = S.Right x
textElem :: Char -> T.Text -> Bool
textElem c = T.any (==c)
getDirectoryContents :: T.Text -> IO [T.Text]
getDirectoryContents fpath = do
h <- openDirStream (T.encodeUtf8 fpath)
let readHandle = do
fp <- readDirStream h
if BS.null fp
then return []
else fmap (T.decodeUtf8 fp :) readHandle
out <- readHandle
closeDirStream h
return out
-- | See System.FilePath.Posix
takeBaseName :: T.Text -> T.Text
takeBaseName fullname =
let afterLastSlash = last $ T.splitOn "/" fullname
splitExtension = init $ T.splitOn "." afterLastSlash
in T.intercalate "." splitExtension
-- | See System.FilePath.Posix
takeDirectory :: T.Text -> T.Text
takeDirectory "" = "."
takeDirectory "/" = "/"
takeDirectory x =
let res = T.dropWhileEnd (== '/') file
file = dropFileName x
in if T.null res && not (T.null file)
then file
else res
-- | Drop the filename.
--
-- > dropFileName x == fst (splitFileName x)
--
-- (See System.FilePath.Posix)
dropFileName :: T.Text -> T.Text
dropFileName = fst . splitFileName
-- | Split a filename into directory and file. 'combine' is the inverse.
--
-- > Valid x => uncurry (</>) (splitFileName x) == x || fst (splitFileName x) == "./"
-- > Valid x => isValid (fst (splitFileName x))
-- > splitFileName "file/bob.txt" == ("file/", "bob.txt")
-- > splitFileName "file/" == ("file/", "")
-- > splitFileName "bob" == ("./", "bob")
-- > Posix: splitFileName "/" == ("/","")
-- > Windows: splitFileName "c:" == ("c:","")
--
-- (See System.FilePath.Posix)
splitFileName :: T.Text -> (T.Text, T.Text)
splitFileName x = (if T.null dir then "./" else dir, name)
where
(dir, name) = splitFileName_ x
splitFileName_ y = (T.reverse b, T.reverse a)
where
(a,b) = T.break (=='/') $ T.reverse y
-- | Read a yaml file and throw a runtime error if the parsing fails
loadYamlFile :: Y.FromJSON a => FilePath -> IO a
loadYamlFile fp = Y.decodeFileEither fp >>= \case
Left rr -> error ("Error when parsing " ++ fp ++ ": " ++ show rr)
Right x -> return x
-- | helper for hashmap, in case we want another kind of map ..
ifromList :: (Monoid m, At m, F.Foldable f) => f (Index m, IxValue m) -> m
{-# INLINABLE ifromList #-}
ifromList = F.foldl' (\curm (k,v) -> curm & at k ?~ v) mempty
ikeys :: (Eq k, Hashable k) => HM.HashMap k v -> HS.HashSet k
{-# INLINABLE ikeys #-}
ikeys = HS.fromList . HM.keys
isingleton :: (Monoid b, At b) => Index b -> IxValue b -> b
{-# INLINABLE isingleton #-}
isingleton k v = mempty & at k ?~ v
ifromListWith :: (Monoid m, At m, F.Foldable f) => (IxValue m -> IxValue m -> IxValue m) -> f (Index m, IxValue m) -> m
{-# INLINABLE ifromListWith #-}
ifromListWith f = F.foldl' (\curmap (k,v) -> iinsertWith f k v curmap) mempty
iinsertWith :: At m => (IxValue m -> IxValue m -> IxValue m) -> Index m -> IxValue m -> m -> m
{-# INLINABLE iinsertWith #-}
iinsertWith f k v m = m & at k %~ mightreplace
where
mightreplace Nothing = Just v
mightreplace (Just x) = Just (f v x)
iunionWith :: (Hashable k, Eq k) => (v -> v -> v) -> HM.HashMap k v -> HM.HashMap k v -> HM.HashMap k v
{-# INLINABLE iunionWith #-}
iunionWith = HM.unionWith
getFiles :: T.Text -> T.Text -> T.Text -> IO [T.Text]
getFiles moduledir subdir extension = fmap concat $
getDirContents moduledir
>>= mapM ( checkForSubFiles extension . (\x -> moduledir <> "/" <> x <> "/" <> subdir))
checkForSubFiles :: T.Text -> T.Text -> IO [T.Text]
checkForSubFiles extension dir =
catch (fmap Right (getDirContents dir)) (\e -> return $ Left (e :: IOException)) >>= \case
Right o -> return ((map (\x -> dir <> "/" <> x) . filter (T.isSuffixOf extension)) o )
Left _ -> return []
getDirContents :: T.Text -> IO [T.Text]
getDirContents x = fmap (filter (not . T.all (=='.'))) (getDirectoryContents x)