{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE BlockArguments #-}
module Data.JSON.Directory
( decodeDirectory
, ModifiedWhileReading
) where
import Control.Exception
import Control.Monad
import Control.Monad.IO.Class
import Data.Aeson
import Data.HashMap.Strict
import Data.List
import Data.Maybe
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.IO as Text
import System.Directory
import System.FilePath
data ModifiedWhileReading = ModifiedWhileReading FilePath
deriving (Show)
instance Exception ModifiedWhileReading
data EntryType
= Directory
| JSON
| TextFile
pathType :: FilePath -> IO (Text, EntryType)
pathType p = do
doesDirectoryExist p >>= \case
True -> pure (Text.pack $ takeFileName p, Directory)
False -> pure case splitExtension (takeFileName p) of
(name, ".json") -> (Text.pack $ name, JSON)
_ -> (Text.pack $ takeFileName p, TextFile)
decodeDirectoryValue :: MonadIO io => FilePath -> io (Either String Value)
decodeDirectoryValue path = liftIO $ do
time <- getModificationTime path
ents <- listDirectory path
kvs <- catMaybes <$> forM ents \ent -> do
if "." `isPrefixOf` ent
then pure Nothing
else Just <$> do
let path' = path </> ent
pathType path' >>= \case
(n, Directory) -> (n,) <$> decodeDirectoryValue path'
(n, JSON ) -> (n,) <$> eitherDecodeFileStrict path'
(n, TextFile ) -> (n,) . Right . String <$> Text.readFile path'
time2 <- getModificationTime path
unless (time == time2) $ throwIO (ModifiedWhileReading path)
pure $ Object <$> sequence (Data.HashMap.Strict.fromList kvs)
resultToEither :: Result a -> Either String a
resultToEither (Success a) = Right a
resultToEither (Error s) = Left s
decodeDirectory :: (FromJSON a, MonadIO io) => FilePath -> io (Either String a)
decodeDirectory p = do
ev <- decodeDirectoryValue p
pure $ do
v <- ev
resultToEither $ fromJSON v