{-# 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 -- | Takes a directory and decodes it using a @`FromJSON`@ instance. -- Each entry in the directory becomes a key, and the contents become -- the corresponding value. -- -- * Directories are recursed into. -- * Files ending in @.json@ are decoded as JSON values. -- * Everything else is assumed to be a valid unicode string. -- -- This function can throw IO exceptions as well as a @`ModifiedWhileReading`@ -- exception if the modification time changes during processing. decodeDirectory :: (FromJSON a, MonadIO io) => FilePath -> io (Either String a) decodeDirectory p = do ev <- decodeDirectoryValue p pure $ do v <- ev resultToEither $ fromJSON v