{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} -- | Database interpreter based on a JSON file module Imm.Database.JsonFile (module Imm.Database.JsonFile, module Reexport) where -- {{{ Imports import Imm.Database hiding (commit, delete, fetchAll, insert, purge, update) import Imm.Database.FeedTable as Reexport import Imm.Error import Imm.Prelude hiding (catch, delete, keys) import Data.Aeson import qualified Data.Map as Map import qualified Data.Set as Set import System.Directory import System.FilePath import System.IO (IOMode (..), openFile) -- }}} -- * Types data CacheStatus = Empty | Clean | Dirty deriving(Eq, Show) data JsonFileDatabase t = JsonFileDatabase FilePath (Map (Key t) (Entry t)) CacheStatus instance Pretty (JsonFileDatabase t) where pretty (JsonFileDatabase file _ _) = "JSON database: " <+> text file mkJsonFileDatabase :: (Table t) => FilePath -> JsonFileDatabase t mkJsonFileDatabase file = JsonFileDatabase file mempty Empty -- | Default database is stored in @$XDG_CONFIG_HOME\/imm\/feeds.json@ defaultDatabase :: Table t => IO (JsonFileDatabase t) defaultDatabase = mkJsonFileDatabase <$> getXdgDirectory XdgConfig "imm/feeds.json" data JsonException = UnableDecode deriving(Eq, Show) instance Exception JsonException where displayException _ = "Unable to parse JSON" -- * Interpreter -- | Interpreter for 'DatabaseF' mkCoDatabase :: (Table t, FromJSON (Key t), FromJSON (Entry t), ToJSON (Key t), ToJSON (Entry t), MonadIO m, MonadCatch m) => JsonFileDatabase t -> CoDatabaseF t m (JsonFileDatabase t) mkCoDatabase t = CoDatabaseF coDescribe coFetch coFetchAll coUpdate coInsert coDelete coPurge coCommit where coDescribe = return (pretty t, t) coFetch keys = do (cache, t') <- coFetchAll let result = fmap (Map.filterWithKey (\uri _ -> member uri $ Set.fromList keys)) cache return (result, t') coFetchAll = handleAny (\e -> return (Left e, t)) $ do t'@(JsonFileDatabase _ cache _) <- loadInCache t return (Right cache, t') coUpdate key f = exec (\a -> update a key f) coInsert rows = exec (`insert` rows) coDelete keys = exec (`delete` keys) coPurge = exec purge coCommit = exec commit exec f = handleAny (\e -> return (Left e, t)) $ (Right (),) <$> f t -- * Low-level implementation loadInCache :: (Table t, MonadIO m, MonadCatch m, FromJSON (Key t), FromJSON (Entry t)) => JsonFileDatabase t -> m (JsonFileDatabase t) loadInCache t@(JsonFileDatabase file _ status) = case status of Empty -> do io $ createDirectoryIfMissing True $ takeDirectory file fileContent <- hGetContents =<< io (openFile file ReadWriteMode) cache <- (`failWith` UnableDecode) $ fmap Map.fromList $ decode $ fromEmpty "[]" fileContent return $ JsonFileDatabase file cache Clean _ -> return t where fromEmpty x "" = x fromEmpty _ y = y insert :: (Table t, MonadIO m, MonadCatch m, FromJSON (Key t), FromJSON (Entry t)) => JsonFileDatabase t -> [(Key t, Entry t)] -> m (JsonFileDatabase t) insert t rows = insertInCache rows <$> loadInCache t insertInCache :: Table t => [(Key t, Entry t)] -> JsonFileDatabase t -> JsonFileDatabase t insertInCache rows (JsonFileDatabase file cache _) = JsonFileDatabase file (Map.union cache $ Map.fromList rows) Dirty update :: (Table t, MonadIO m, MonadCatch m, FromJSON (Key t), FromJSON (Entry t)) => JsonFileDatabase t -> Key t -> (Entry t -> Entry t) -> m (JsonFileDatabase t) update t key f = updateInCache key f <$> loadInCache t updateInCache :: Table t => Key t -> (Entry t -> Entry t) -> JsonFileDatabase t -> JsonFileDatabase t updateInCache key f (JsonFileDatabase file cache _) = JsonFileDatabase file newCache Dirty where newCache = Map.update (Just . f) key cache delete :: (Table t, MonadIO m, MonadCatch m, FromJSON (Key t), FromJSON (Entry t)) => JsonFileDatabase t -> [Key t] -> m (JsonFileDatabase t) delete t keys = deleteInCache keys <$> loadInCache t deleteInCache :: Table t => [Key t] -> JsonFileDatabase t -> JsonFileDatabase t deleteInCache keys (JsonFileDatabase file cache _) = JsonFileDatabase file newCache Dirty where newCache = foldr Map.delete cache keys purge :: (Table t, MonadIO m, MonadCatch m, FromJSON (Key t), FromJSON (Entry t)) => JsonFileDatabase t -> m (JsonFileDatabase t) purge t = purgeInCache <$> loadInCache t purgeInCache :: Table t => JsonFileDatabase t -> JsonFileDatabase t purgeInCache (JsonFileDatabase file _ _) = JsonFileDatabase file mempty Dirty commit :: (MonadIO m, ToJSON (Key t), ToJSON (Entry t)) => JsonFileDatabase t -> m (JsonFileDatabase t) commit t@(JsonFileDatabase file cache status) = case status of Dirty -> do writeFile file $ encode $ Map.toList cache return $ JsonFileDatabase file cache Clean _ -> return t