packages feed

imm 1.2.1.0 → 1.3.0.0

raw patch · 25 files changed

+721/−897 lines, 25 filesdep +lifted-basedep +monad-timedep +mtldep −ansi-wl-pprintdep −chunked-datadep −comonadPVP ok

version bump matches the API change (PVP)

Dependencies added: lifted-base, monad-time, mtl, prettyprinter, prettyprinter-ansi-terminal, stm, streaming-bytestring, streaming-with, streamly, transformers-base

Dependencies removed: ansi-wl-pprint, chunked-data, comonad, conduit-combinators, free, rainbow, rainbox, tagged

API changes (from Hackage documentation)

- Imm.Database: CoDatabaseF :: m (Doc, a) -> ([Key t] -> m (Either SomeException (Map (Key t) (Entry t)), a)) -> m (Either SomeException (Map (Key t) (Entry t)), a) -> (Key t -> (Entry t -> Entry t) -> m (Either SomeException (), a)) -> ([(Key t, Entry t)] -> m (Either SomeException (), a)) -> ([Key t] -> m (Either SomeException (), a)) -> m (Either SomeException (), a) -> m (Either SomeException (), a) -> CoDatabaseF t m a
- Imm.Database: Commit :: t -> (Either SomeException () -> next) -> DatabaseF t next
- Imm.Database: DeleteList :: t -> [Key t] -> (Either SomeException () -> next) -> DatabaseF t next
- Imm.Database: Describe :: t -> (Doc -> next) -> DatabaseF t next
- Imm.Database: FetchAll :: t -> (Either SomeException (Map (Key t) (Entry t)) -> next) -> DatabaseF t next
- Imm.Database: FetchList :: t -> [Key t] -> (Either SomeException (Map (Key t) (Entry t)) -> next) -> DatabaseF t next
- Imm.Database: InsertList :: t -> [(Key t, Entry t)] -> (Either SomeException () -> next) -> DatabaseF t next
- Imm.Database: Purge :: t -> (Either SomeException () -> next) -> DatabaseF t next
- Imm.Database: Update :: t -> (Key t) -> (Entry t -> Entry t) -> (Either SomeException () -> next) -> DatabaseF t next
- Imm.Database: [commitH] :: CoDatabaseF t m a -> m (Either SomeException (), a)
- Imm.Database: [deleteListH] :: CoDatabaseF t m a -> [Key t] -> m (Either SomeException (), a)
- Imm.Database: [describeH] :: CoDatabaseF t m a -> m (Doc, a)
- Imm.Database: [fetchAllH] :: CoDatabaseF t m a -> m (Either SomeException (Map (Key t) (Entry t)), a)
- Imm.Database: [fetchListH] :: CoDatabaseF t m a -> [Key t] -> m (Either SomeException (Map (Key t) (Entry t)), a)
- Imm.Database: [insertListH] :: CoDatabaseF t m a -> [(Key t, Entry t)] -> m (Either SomeException (), a)
- Imm.Database: [purgeH] :: CoDatabaseF t m a -> m (Either SomeException (), a)
- Imm.Database: [updateH] :: CoDatabaseF t m a -> Key t -> (Entry t -> Entry t) -> m (Either SomeException (), a)
- Imm.Database: data CoDatabaseF t m a
- Imm.Database: data DatabaseF t next
- Imm.Database: describeDatabase :: (MonadFree f m, DatabaseF t :<: f) => t -> m Doc
- Imm.Database: instance (Imm.Database.Table t, GHC.Show.Show (Imm.Database.Key t), GHC.Show.Show (Imm.Database.Entry t), Text.PrettyPrint.ANSI.Leijen.Internal.Pretty (Imm.Database.Key t), Data.Typeable.Internal.Typeable t) => GHC.Exception.Exception (Imm.Database.DatabaseException t)
- Imm.Database: instance (Text.PrettyPrint.ANSI.Leijen.Internal.Pretty t, Text.PrettyPrint.ANSI.Leijen.Internal.Pretty (Imm.Database.Key t)) => Text.PrettyPrint.ANSI.Leijen.Internal.Pretty (Imm.Database.DatabaseException t)
- Imm.Database: instance GHC.Base.Functor (Imm.Database.DatabaseF t)
- Imm.Database: instance GHC.Base.Functor m => GHC.Base.Functor (Imm.Database.CoDatabaseF t m)
- Imm.Database: instance GHC.Base.Monad m => Imm.Prelude.PairingM (Imm.Database.CoDatabaseF t m) (Imm.Database.DatabaseF t) m
- Imm.Database.FeedTable: [entryCategory] :: DatabaseEntry -> Text
- Imm.Database.FeedTable: data Database
- Imm.Database.FeedTable: instance Text.PrettyPrint.ANSI.Leijen.Internal.Pretty Imm.Database.FeedTable.DatabaseEntry
- Imm.Database.FeedTable: instance Text.PrettyPrint.ANSI.Leijen.Internal.Pretty Imm.Database.FeedTable.FeedID
- Imm.Database.FeedTable: instance Text.PrettyPrint.ANSI.Leijen.Internal.Pretty Imm.Database.FeedTable.FeedStatus
- Imm.Database.FeedTable: instance Text.PrettyPrint.ANSI.Leijen.Internal.Pretty Imm.Database.FeedTable.FeedTable
- Imm.Database.FeedTable: type CoDatabaseF' = CoDatabaseF FeedTable
- Imm.Database.FeedTable: type DatabaseF' = DatabaseF FeedTable
- Imm.Database.JsonFile: Clean :: CacheStatus
- Imm.Database.JsonFile: Dirty :: CacheStatus
- Imm.Database.JsonFile: Empty :: CacheStatus
- Imm.Database.JsonFile: JsonFileDatabase :: FilePath -> (Map (Key t) (Entry t)) -> CacheStatus -> JsonFileDatabase t
- Imm.Database.JsonFile: commit :: (MonadIO m, ToJSON (Key t), ToJSON (Entry t)) => JsonFileDatabase t -> m (JsonFileDatabase t)
- Imm.Database.JsonFile: data CacheStatus
- Imm.Database.JsonFile: delete :: (Table t, MonadIO m, MonadCatch m, FromJSON (Key t), FromJSON (Entry t)) => JsonFileDatabase t -> [Key t] -> m (JsonFileDatabase t)
- Imm.Database.JsonFile: deleteInCache :: Table t => [Key t] -> JsonFileDatabase t -> JsonFileDatabase t
- Imm.Database.JsonFile: insert :: (Table t, MonadIO m, MonadCatch m, FromJSON (Key t), FromJSON (Entry t)) => JsonFileDatabase t -> [(Key t, Entry t)] -> m (JsonFileDatabase t)
- Imm.Database.JsonFile: insertInCache :: Table t => [(Key t, Entry t)] -> JsonFileDatabase t -> JsonFileDatabase t
- Imm.Database.JsonFile: instance Text.PrettyPrint.ANSI.Leijen.Internal.Pretty (Imm.Database.JsonFile.JsonFileDatabase t)
- Imm.Database.JsonFile: loadInCache :: (Table t, MonadIO m, MonadCatch m, FromJSON (Key t), FromJSON (Entry t)) => JsonFileDatabase t -> m (JsonFileDatabase t)
- Imm.Database.JsonFile: 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)
- Imm.Database.JsonFile: purge :: (Table t, MonadIO m, MonadCatch m, FromJSON (Key t), FromJSON (Entry t)) => JsonFileDatabase t -> m (JsonFileDatabase t)
- Imm.Database.JsonFile: purgeInCache :: Table t => JsonFileDatabase t -> JsonFileDatabase t
- Imm.Database.JsonFile: update :: (Table t, MonadIO m, MonadCatch m, FromJSON (Key t), FromJSON (Entry t)) => JsonFileDatabase t -> Key t -> (Entry t -> Entry t) -> m (JsonFileDatabase t)
- Imm.Database.JsonFile: updateInCache :: Table t => Key t -> (Entry t -> Entry t) -> JsonFileDatabase t -> JsonFileDatabase t
- Imm.Feed: instance Text.PrettyPrint.ANSI.Leijen.Internal.Pretty Imm.Feed.FeedRef
- Imm.HTTP: CoHttpClientF :: (URI -> m (Either SomeException LByteString, a)) -> CoHttpClientF m a
- Imm.HTTP: Get :: URI -> (Either SomeException LByteString -> next) -> HttpClientF next
- Imm.HTTP: [getH] :: CoHttpClientF m a -> URI -> m (Either SomeException LByteString, a)
- Imm.HTTP: data HttpClientF next
- Imm.HTTP: instance GHC.Base.Functor Imm.HTTP.HttpClientF
- Imm.HTTP: instance GHC.Base.Functor m => GHC.Base.Functor (Imm.HTTP.CoHttpClientF m)
- Imm.HTTP: instance GHC.Base.Monad m => Imm.Prelude.PairingM (Imm.HTTP.CoHttpClientF m) Imm.HTTP.HttpClientF m
- Imm.HTTP: newtype CoHttpClientF m a
- Imm.HTTP.Simple: mkCoHttpClient :: (MonadIO m, MonadCatch m) => Manager -> CoHttpClientF m Manager
- Imm.Hooks: CoHooksF :: (Feed -> FeedElement -> m a) -> CoHooksF m a
- Imm.Hooks: OnNewElement :: Feed -> FeedElement -> next -> HooksF next
- Imm.Hooks: [onNewElementH] :: CoHooksF m a -> Feed -> FeedElement -> m a
- Imm.Hooks: data CoHooksF m a
- Imm.Hooks: data HooksF next
- Imm.Hooks: instance GHC.Base.Functor Imm.Hooks.HooksF
- Imm.Hooks: instance GHC.Base.Functor m => GHC.Base.Functor (Imm.Hooks.CoHooksF m)
- Imm.Hooks: instance GHC.Base.Monad m => Imm.Prelude.PairingM (Imm.Hooks.CoHooksF m) Imm.Hooks.HooksF m
- Imm.Hooks.SendMail: mkCoHooks :: (MonadIO m) => SendMailSettings -> CoHooksF m SendMailSettings
- Imm.Hooks.WriteFile: data WriteFileSettings
- Imm.Hooks.WriteFile: mkCoHooks :: MonadIO m => WriteFileSettings -> CoHooksF m WriteFileSettings
- Imm.Logger: CoLoggerF :: (LogLevel -> Doc -> m a) -> m (LogLevel, a) -> (LogLevel -> m a) -> (Bool -> m a) -> m a -> CoLoggerF m a
- Imm.Logger: Flush :: next -> LoggerF next
- Imm.Logger: GetLevel :: (LogLevel -> next) -> LoggerF next
- Imm.Logger: Log :: LogLevel -> Doc -> next -> LoggerF next
- Imm.Logger: SetColorize :: Bool -> next -> LoggerF next
- Imm.Logger: SetLevel :: LogLevel -> next -> LoggerF next
- Imm.Logger: [flushH] :: CoLoggerF m a -> m a
- Imm.Logger: [getLevelH] :: CoLoggerF m a -> m (LogLevel, a)
- Imm.Logger: [logH] :: CoLoggerF m a -> LogLevel -> Doc -> m a
- Imm.Logger: [setColorizeH] :: CoLoggerF m a -> Bool -> m a
- Imm.Logger: [setLevelH] :: CoLoggerF m a -> LogLevel -> m a
- Imm.Logger: data CoLoggerF m a
- Imm.Logger: data LoggerF next
- Imm.Logger: instance GHC.Base.Functor Imm.Logger.LoggerF
- Imm.Logger: instance GHC.Base.Functor m => GHC.Base.Functor (Imm.Logger.CoLoggerF m)
- Imm.Logger: instance GHC.Base.Monad m => Imm.Prelude.PairingM (Imm.Logger.CoLoggerF m) Imm.Logger.LoggerF m
- Imm.Logger: instance Text.PrettyPrint.ANSI.Leijen.Internal.Pretty Imm.Logger.LogLevel
- Imm.Logger.Simple: [colorizeLogs] :: LoggerSettings -> Bool
- Imm.Logger.Simple: [errorLoggerSet] :: LoggerSettings -> LoggerSet
- Imm.Logger.Simple: [logLevel] :: LoggerSettings -> LogLevel
- Imm.Logger.Simple: [loggerSet] :: LoggerSettings -> LoggerSet
- Imm.Logger.Simple: mkCoLogger :: (MonadIO m) => LoggerSettings -> CoLoggerF m LoggerSettings
- Imm.Prelude: (*:) :: (Functor f, Functor g) => (a -> f a) -> (b -> g b) -> (a, b) -> Product f g (a, b)
- Imm.Prelude: (+:) :: a -> b -> (a, b)
- Imm.Prelude: (<++>) :: Doc -> Doc -> Doc
- Imm.Prelude: class (Functor sub, Functor sup) => (:<:) sub sup
- Imm.Prelude: class (Monad m, Functor f, Functor g) => PairingM f g m | f -> g
- Imm.Prelude: class Sub i sub sup
- Imm.Prelude: data HId
- Imm.Prelude: data HLeft
- Imm.Prelude: data HNo
- Imm.Prelude: data HRight
- Imm.Prelude: infixr 0 *:
- Imm.Prelude: inj :: (:<:) sub sup => sub a -> sup a
- Imm.Prelude: inj' :: Sub i sub sup => Tagged i (sub a -> sup a)
- Imm.Prelude: instance (GHC.Base.Functor f, GHC.Base.Functor g, Imm.Prelude.Sub (Imm.Prelude.Contains f g) f g) => f Imm.Prelude.:<: g
- Imm.Prelude: instance (Imm.Prelude.PairingM f f' m, Imm.Prelude.PairingM g g' m) => Imm.Prelude.PairingM (Data.Functor.Product.Product f g) (Data.Functor.Sum.Sum f' g') m
- Imm.Prelude: instance (Imm.Prelude.PairingM f f' m, Imm.Prelude.PairingM g g' m) => Imm.Prelude.PairingM (Data.Functor.Sum.Sum f g) (Data.Functor.Product.Product f' g') m
- Imm.Prelude: instance GHC.Base.Monad m => Imm.Prelude.PairingM Data.Functor.Identity.Identity Data.Functor.Identity.Identity m
- Imm.Prelude: instance Imm.Prelude.Sub Imm.Prelude.HId a a
- Imm.Prelude: instance Imm.Prelude.Sub Imm.Prelude.HLeft a (Data.Functor.Sum.Sum a b)
- Imm.Prelude: instance Imm.Prelude.Sub x f g => Imm.Prelude.Sub (Imm.Prelude.HRight, x) f (Data.Functor.Sum.Sum h g)
- Imm.Prelude: interpret :: (PairingM f g m) => (a -> b -> m r) -> Cofree f a -> FreeT g m b -> m r
- Imm.Prelude: pairM :: PairingM f g m => (a -> b -> m r) -> f a -> g b -> m r
- Imm.Prelude: type (:::) a b = (a, b)
- Imm.XML: CoXmlParserF :: (URI -> LByteString -> m (Either SomeException Feed, a)) -> CoXmlParserF m a
- Imm.XML: ParseXml :: URI -> LByteString -> (Either SomeException Feed -> next) -> XmlParserF next
- Imm.XML: [parseXmlH] :: CoXmlParserF m a -> URI -> LByteString -> m (Either SomeException Feed, a)
- Imm.XML: data XmlParserF next
- Imm.XML: instance GHC.Base.Functor Imm.XML.XmlParserF
- Imm.XML: instance GHC.Base.Functor m => GHC.Base.Functor (Imm.XML.CoXmlParserF m)
- Imm.XML: instance GHC.Base.Monad m => Imm.Prelude.PairingM (Imm.XML.CoXmlParserF m) Imm.XML.XmlParserF m
- Imm.XML: newtype CoXmlParserF m a
- Imm.XML.Simple: defaultPreProcess :: Monad m => PreProcess m
- Imm.XML.Simple: mkCoXmlParser :: (MonadIO m, MonadCatch m) => PreProcess m -> CoXmlParserF m (PreProcess m)
- Imm.XML.Simple: type PreProcess m = URI -> Conduit Event m Event
+ Imm.Boot: Modules :: httpClient -> databaseClient -> logger -> hooks -> xmlParser -> Modules httpClient databaseClient logger hooks xmlParser
+ Imm.Boot: [_databaseClient] :: Modules httpClient databaseClient logger hooks xmlParser -> databaseClient
+ Imm.Boot: [_hooks] :: Modules httpClient databaseClient logger hooks xmlParser -> hooks
+ Imm.Boot: [_httpClient] :: Modules httpClient databaseClient logger hooks xmlParser -> httpClient
+ Imm.Boot: [_logger] :: Modules httpClient databaseClient logger hooks xmlParser -> logger
+ Imm.Boot: [_xmlParser] :: Modules httpClient databaseClient logger hooks xmlParser -> xmlParser
+ Imm.Boot: data Modules httpClient databaseClient logger hooks xmlParser
+ Imm.Boot: data ModulesM m
+ Imm.Boot: instance (Control.Monad.Catch.MonadThrow m, Imm.Database.MonadDatabase Imm.Database.FeedTable.FeedTable (Control.Monad.Trans.Reader.ReaderT b m)) => Imm.Database.MonadDatabase Imm.Database.FeedTable.FeedTable (Control.Monad.Trans.Reader.ReaderT (Imm.Boot.Modules a b c d e) m)
+ Imm.Boot: instance (Control.Monad.Catch.MonadThrow m, Imm.HTTP.MonadHttpClient (Control.Monad.Trans.Reader.ReaderT a m)) => Imm.HTTP.MonadHttpClient (Control.Monad.Trans.Reader.ReaderT (Imm.Boot.Modules a b c d e) m)
+ Imm.Boot: instance (Control.Monad.Catch.MonadThrow m, Imm.XML.MonadXmlParser (Control.Monad.Trans.Reader.ReaderT e m)) => Imm.XML.MonadXmlParser (Control.Monad.Trans.Reader.ReaderT (Imm.Boot.Modules a b c d e) m)
+ Imm.Boot: instance (Control.Monad.IO.Class.MonadIO m, Imm.Logger.MonadLog (Control.Monad.Trans.Reader.ReaderT c m)) => Imm.Logger.MonadLog (Control.Monad.Trans.Reader.ReaderT (Imm.Boot.Modules a b c d e) m)
+ Imm.Boot: instance (GHC.Base.Monad m, Imm.Hooks.MonadImm (Control.Monad.Trans.Reader.ReaderT d m)) => Imm.Hooks.MonadImm (Control.Monad.Trans.Reader.ReaderT (Imm.Boot.Modules a b c d e) m)
+ Imm.Boot: mkModulesM :: (MonadXmlParser (ReaderT e m), MonadImm (ReaderT d m), MonadLog (ReaderT c m), MonadDatabase FeedTable (ReaderT b m), MonadHttpClient (ReaderT a m)) => a -> b -> c -> d -> e -> ModulesM m
+ Imm.Database: _commit :: MonadDatabase t m => t -> m ()
+ Imm.Database: _deleteList :: MonadDatabase t m => t -> [Key t] -> m ()
+ Imm.Database: _describeDatabase :: MonadDatabase t m => t -> m (Doc a)
+ Imm.Database: _fetchAll :: MonadDatabase t m => t -> m (Map (Key t) (Entry t))
+ Imm.Database: _fetchList :: MonadDatabase t m => t -> [Key t] -> m (Map (Key t) (Entry t))
+ Imm.Database: _insertList :: MonadDatabase t m => t -> [(Key t, Entry t)] -> m ()
+ Imm.Database: _purge :: MonadDatabase t m => t -> m ()
+ Imm.Database: _update :: MonadDatabase t m => t -> Key t -> (Entry t -> Entry t) -> m ()
+ Imm.Database: class MonadThrow m => MonadDatabase t m
+ Imm.Database: instance (Data.Text.Prettyprint.Doc.Internal.Pretty t, Data.Text.Prettyprint.Doc.Internal.Pretty (Imm.Database.Key t)) => Data.Text.Prettyprint.Doc.Internal.Pretty (Imm.Database.DatabaseException t)
+ Imm.Database: instance (Imm.Database.Table t, GHC.Show.Show (Imm.Database.Key t), GHC.Show.Show (Imm.Database.Entry t), Data.Text.Prettyprint.Doc.Internal.Pretty (Imm.Database.Key t), Data.Typeable.Internal.Typeable t) => GHC.Exception.Exception (Imm.Database.DatabaseException t)
+ Imm.Database.FeedTable: [entryTags] :: DatabaseEntry -> Set Text
+ Imm.Database.FeedTable: instance Data.Text.Prettyprint.Doc.Internal.Pretty Imm.Database.FeedTable.FeedID
+ Imm.Database.FeedTable: instance Data.Text.Prettyprint.Doc.Internal.Pretty Imm.Database.FeedTable.FeedStatus
+ Imm.Database.FeedTable: instance Data.Text.Prettyprint.Doc.Internal.Pretty Imm.Database.FeedTable.FeedTable
+ Imm.Database.FeedTable: newtype Database
+ Imm.Database.FeedTable: prettyDatabaseEntry :: DatabaseEntry -> Doc AnsiStyle
+ Imm.Database.FeedTable: prettyFeedID :: FeedID -> Doc AnsiStyle
+ Imm.Database.JsonFile: instance (Imm.Database.Table t, Data.Aeson.Types.FromJSON.FromJSON (Imm.Database.Key t), Data.Aeson.Types.FromJSON.FromJSON (Imm.Database.Entry t), Data.Aeson.Types.ToJSON.ToJSON (Imm.Database.Key t), Data.Aeson.Types.ToJSON.ToJSON (Imm.Database.Entry t)) => Imm.Database.MonadDatabase t (Control.Monad.Trans.Reader.ReaderT (GHC.MVar.MVar (Imm.Database.JsonFile.JsonFileDatabase t)) GHC.Types.IO)
+ Imm.Database.JsonFile: instance Data.Text.Prettyprint.Doc.Internal.Pretty (Imm.Database.JsonFile.JsonFileDatabase t)
+ Imm.Feed: instance Data.Text.Prettyprint.Doc.Internal.Pretty Imm.Feed.FeedRef
+ Imm.HTTP: class MonadThrow m => MonadHttpClient m
+ Imm.HTTP: httpGet :: MonadHttpClient m => URI -> m LByteString
+ Imm.HTTP.Simple: instance Imm.HTTP.MonadHttpClient (Control.Monad.Trans.Reader.ReaderT Network.HTTP.Client.Types.Manager GHC.Types.IO)
+ Imm.Hooks: class Monad m => MonadImm m
+ Imm.Hooks: processNewElement :: MonadImm m => Feed -> FeedElement -> m ()
+ Imm.Hooks.Dummy: DummyHooks :: DummyHooks
+ Imm.Hooks.Dummy: data DummyHooks
+ Imm.Hooks.Dummy: instance Imm.Hooks.MonadImm (Control.Monad.Trans.Reader.ReaderT Imm.Hooks.Dummy.DummyHooks GHC.Types.IO)
+ Imm.Hooks.SendMail: instance Imm.Hooks.MonadImm (Control.Monad.Trans.Reader.ReaderT Imm.Hooks.SendMail.SendMailSettings GHC.Types.IO)
+ Imm.Hooks.WriteFile: instance Imm.Hooks.MonadImm (Control.Monad.Trans.Reader.ReaderT Imm.Hooks.WriteFile.WriteFileSettings GHC.Types.IO)
+ Imm.Hooks.WriteFile: newtype WriteFileSettings
+ Imm.Logger: class Monad m => MonadLog m
+ Imm.Logger: instance Data.Text.Prettyprint.Doc.Internal.Pretty Imm.Logger.LogLevel
+ Imm.Logger.Simple: [_colorizeLogs] :: LoggerSettings -> Bool
+ Imm.Logger.Simple: [_errorLoggerSet] :: LoggerSettings -> LoggerSet
+ Imm.Logger.Simple: [_logLevel] :: LoggerSettings -> LogLevel
+ Imm.Logger.Simple: [_loggerSet] :: LoggerSettings -> LoggerSet
+ Imm.Logger.Simple: instance Imm.Logger.MonadLog (Control.Monad.Trans.Reader.ReaderT (GHC.MVar.MVar Imm.Logger.Simple.LoggerSettings) GHC.Types.IO)
+ Imm.XML: class MonadThrow m => MonadXmlParser m
+ Imm.XML.Conduit: XmlParser :: (forall m. Monad m => URI -> ConduitT Event Event m ()) -> XmlParser
+ Imm.XML.Conduit: defaultXmlParser :: XmlParser
+ Imm.XML.Conduit: instance (Control.Monad.IO.Class.MonadIO m, Control.Monad.Catch.MonadCatch m) => Imm.XML.MonadXmlParser (Control.Monad.Trans.Reader.ReaderT Imm.XML.Conduit.XmlParser m)
+ Imm.XML.Conduit: newtype XmlParser
- Imm.Boot: imm :: (a -> CoHttpClientF IO a, a) -> (b -> CoDatabaseF' IO b, b) -> (c -> CoLoggerF IO c, c) -> (d -> CoHooksF IO d, d) -> (e -> CoXmlParserF IO e, e) -> IO ()
+ Imm.Boot: imm :: ModulesM IO -> IO ()
- Imm.Core: check :: (MonadIO m, MonadCatch m, LoggerF :<: f, MonadFree f m, DatabaseF' :<: f, HttpClientF :<: f, XmlParserF :<: f) => [FeedID] -> m ()
+ Imm.Core: check :: (MonadAsync m, MonadCatch m, MonadLog m, MonadDatabase FeedTable m, MonadHttpClient m, MonadXmlParser m) => [FeedID] -> m ()
- Imm.Core: importOPML :: (MonadIO m, LoggerF :<: f, MonadFree f m, DatabaseF' :<: f, MonadCatch m) => m ()
+ Imm.Core: importOPML :: (MonadLog m, MonadDatabase FeedTable m, MonadCatch m) => ConduitT () ByteString m () -> m ()
- Imm.Core: printVersions :: (MonadIO m) => m ()
+ Imm.Core: printVersions :: (MonadBase IO m) => m ()
- Imm.Core: run :: (MonadIO m, MonadCatch m, HooksF :<: f, LoggerF :<: f, MonadFree f m, DatabaseF' :<: f, HttpClientF :<: f, XmlParserF :<: f) => [FeedID] -> m ()
+ Imm.Core: run :: (MonadTime m, MonadAsync m, MonadCatch m, MonadImm m, MonadLog m, MonadDatabase FeedTable m, MonadHttpClient m, MonadXmlParser m) => [FeedID] -> m ()
- Imm.Core: showFeed :: (MonadIO m, LoggerF :<: f, MonadThrow m, MonadFree f m, DatabaseF' :<: f) => [FeedID] -> m ()
+ Imm.Core: showFeed :: (MonadLog m, MonadThrow m, MonadDatabase FeedTable m) => [FeedID] -> m ()
- Imm.Core: subscribe :: (LoggerF :<: f, MonadFree f m, DatabaseF' :<: f, MonadCatch m) => URI -> Maybe Text -> m ()
+ Imm.Core: subscribe :: (MonadLog m, MonadDatabase FeedTable m, MonadCatch m) => URI -> Set Text -> m ()
- Imm.Database: class (Ord (Key t), Show (Key t), Show (Entry t), Typeable t, Show t, Pretty t, Pretty (Key t), Pretty (Entry t)) => Table t where type Key t :: * type Entry t :: * where {
+ Imm.Database: class (Ord (Key t), Show (Key t), Show (Entry t), Typeable t, Show t, Pretty t, Pretty (Key t)) => Table t where {
- Imm.Database: commit :: (MonadThrow m, MonadFree f m, DatabaseF t :<: f, LoggerF :<: f) => t -> m ()
+ Imm.Database: commit :: (MonadThrow m, MonadDatabase t m, MonadLog m) => t -> m ()
- Imm.Database: delete :: (MonadThrow m, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f) => t -> Key t -> m ()
+ Imm.Database: delete :: (MonadThrow m, MonadDatabase t m, MonadLog m) => t -> Key t -> m ()
- Imm.Database: deleteList :: (MonadThrow m, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f) => t -> [Key t] -> m ()
+ Imm.Database: deleteList :: (MonadThrow m, MonadDatabase t m, MonadLog m) => t -> [Key t] -> m ()
- Imm.Database: fetch :: (MonadFree f m, DatabaseF t :<: f, Table t, MonadThrow m) => t -> Key t -> m (Entry t)
+ Imm.Database: fetch :: (MonadDatabase t m, Table t, MonadThrow m) => t -> Key t -> m (Entry t)
- Imm.Database: fetchAll :: (MonadThrow m, MonadFree f m, DatabaseF t :<: f) => t -> m (Map (Key t) (Entry t))
+ Imm.Database: fetchAll :: (MonadThrow m, MonadDatabase t m) => t -> m (Map (Key t) (Entry t))
- Imm.Database: fetchList :: (MonadFree f m, DatabaseF t :<: f, MonadThrow m) => t -> [Key t] -> m (Map (Key t) (Entry t))
+ Imm.Database: fetchList :: (MonadDatabase t m, MonadThrow m) => t -> [Key t] -> m (Map (Key t) (Entry t))
- Imm.Database: insert :: (MonadThrow m, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f) => t -> Key t -> Entry t -> m ()
+ Imm.Database: insert :: (MonadThrow m, MonadDatabase t m, MonadLog m) => t -> Key t -> Entry t -> m ()
- Imm.Database: insertList :: (MonadThrow m, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f) => t -> [(Key t, Entry t)] -> m ()
+ Imm.Database: insertList :: (MonadThrow m, MonadDatabase t m, MonadLog m) => t -> [(Key t, Entry t)] -> m ()
- Imm.Database: purge :: (MonadThrow m, MonadFree f m, DatabaseF t :<: f, LoggerF :<: f) => t -> m ()
+ Imm.Database: purge :: (MonadThrow m, MonadDatabase t m, MonadLog m) => t -> m ()
- Imm.Database: update :: (MonadFree f m, DatabaseF t :<: f, MonadThrow m) => t -> Key t -> (Entry t -> Entry t) -> m ()
+ Imm.Database: update :: (MonadDatabase t m, MonadThrow m) => t -> Key t -> (Entry t -> Entry t) -> m ()
- Imm.Database.FeedTable: DatabaseEntry :: URI -> Text -> Set Int -> Maybe UTCTime -> DatabaseEntry
+ Imm.Database.FeedTable: DatabaseEntry :: URI -> Set Text -> Set Int -> Maybe UTCTime -> DatabaseEntry
- Imm.Database.FeedTable: addReadHash :: (DatabaseF' :<: f, MonadFree f m, MonadThrow m, LoggerF :<: f) => FeedID -> Int -> m ()
+ Imm.Database.FeedTable: addReadHash :: (MonadDatabase FeedTable m, MonadThrow m, MonadLog m) => FeedID -> Int -> m ()
- Imm.Database.FeedTable: getStatus :: (DatabaseF' :<: f, MonadFree f m, MonadCatch m) => FeedID -> m FeedStatus
+ Imm.Database.FeedTable: getStatus :: (MonadDatabase FeedTable m, MonadCatch m) => FeedID -> m FeedStatus
- Imm.Database.FeedTable: markAsRead :: (MonadIO m, DatabaseF' :<: f, MonadFree f m, MonadThrow m, LoggerF :<: f) => FeedID -> m ()
+ Imm.Database.FeedTable: markAsRead :: (MonadTime m, MonadDatabase FeedTable m, MonadThrow m, MonadLog m) => FeedID -> m ()
- Imm.Database.FeedTable: markAsUnread :: (DatabaseF' :<: f, MonadFree f m, MonadThrow m, LoggerF :<: f) => FeedID -> m ()
+ Imm.Database.FeedTable: markAsUnread :: (MonadDatabase FeedTable m, MonadThrow m, MonadLog m) => FeedID -> m ()
- Imm.Database.FeedTable: newDatabaseEntry :: FeedID -> Text -> DatabaseEntry
+ Imm.Database.FeedTable: newDatabaseEntry :: FeedID -> Set Text -> DatabaseEntry
- Imm.Database.FeedTable: register :: (MonadThrow m, LoggerF :<: f, DatabaseF' :<: f, MonadFree f m) => FeedID -> Text -> m ()
+ Imm.Database.FeedTable: register :: (MonadThrow m, MonadLog m, MonadDatabase FeedTable m) => FeedID -> Set Text -> m ()
- Imm.Database.JsonFile: defaultDatabase :: Table t => IO (JsonFileDatabase t)
+ Imm.Database.JsonFile: defaultDatabase :: Table t => IO (MVar (JsonFileDatabase t))
- Imm.Feed: prettyElement :: FeedElement -> Doc
+ Imm.Feed: prettyElement :: FeedElement -> Doc a
- Imm.HTTP: get :: (MonadFree f m, HttpClientF :<: f, LoggerF :<: f, MonadThrow m) => URI -> m LByteString
+ Imm.HTTP: get :: (MonadHttpClient m, MonadLog m, MonadThrow m) => URI -> m LByteString
- Imm.Hooks: onNewElement :: (MonadFree f m, LoggerF :<: f, HooksF :<: f) => Feed -> FeedElement -> m ()
+ Imm.Hooks: onNewElement :: (MonadImm m, MonadLog m) => Feed -> FeedElement -> m ()
- Imm.Hooks.WriteFile: FileInfo :: FilePath -> ByteString -> FileInfo
+ Imm.Hooks.WriteFile: FileInfo :: FilePath -> Builder -> FileInfo
- Imm.Hooks.WriteFile: convertDoc :: (IsString t) => Doc -> t
+ Imm.Hooks.WriteFile: convertDoc :: (IsString t) => Doc a -> t
- Imm.Hooks.WriteFile: defaultFileContent :: Feed -> FeedElement -> ByteString
+ Imm.Hooks.WriteFile: defaultFileContent :: Feed -> FeedElement -> Builder
- Imm.Logger: flushLogs :: (MonadFree f m, LoggerF :<: f) => m ()
+ Imm.Logger: flushLogs :: MonadLog m => m ()
- Imm.Logger: getLogLevel :: (MonadFree f m, LoggerF :<: f) => m LogLevel
+ Imm.Logger: getLogLevel :: MonadLog m => m LogLevel
- Imm.Logger: log :: (MonadFree f m, LoggerF :<: f) => LogLevel -> Doc -> m ()
+ Imm.Logger: log :: MonadLog m => LogLevel -> Doc AnsiStyle -> m ()
- Imm.Logger: logDebug :: (MonadFree f m, LoggerF :<: f) => Doc -> m ()
+ Imm.Logger: logDebug :: MonadLog m => Doc AnsiStyle -> m ()
- Imm.Logger: logError :: (MonadFree f m, LoggerF :<: f) => Doc -> m ()
+ Imm.Logger: logError :: MonadLog m => Doc AnsiStyle -> m ()
- Imm.Logger: logInfo :: (MonadFree f m, LoggerF :<: f) => Doc -> m ()
+ Imm.Logger: logInfo :: MonadLog m => Doc AnsiStyle -> m ()
- Imm.Logger: logWarning :: (MonadFree f m, LoggerF :<: f) => Doc -> m ()
+ Imm.Logger: logWarning :: MonadLog m => Doc AnsiStyle -> m ()
- Imm.Logger: setColorizeLogs :: (MonadFree f m, LoggerF :<: f) => Bool -> m ()
+ Imm.Logger: setColorizeLogs :: MonadLog m => Bool -> m ()
- Imm.Logger: setLogLevel :: (MonadFree f m, LoggerF :<: f) => LogLevel -> m ()
+ Imm.Logger: setLogLevel :: MonadLog m => LogLevel -> m ()
- Imm.Logger.Simple: defaultLogger :: MonadIO m => m LoggerSettings
+ Imm.Logger.Simple: defaultLogger :: IO (MVar LoggerSettings)
- Imm.XML: parseXml :: (MonadFree f m, XmlParserF :<: f, MonadThrow m) => URI -> LByteString -> m Feed
+ Imm.XML: parseXml :: MonadXmlParser m => URI -> LByteString -> m Feed

Files

imm.cabal view
@@ -1,5 +1,5 @@ name:                imm-version:             1.2.1.0+version:             1.3.0.0 synopsis:            Execute arbitrary actions for each unread element of RSS/Atom feeds description:         Cf README file homepage:            https://github.com/k0ral/imm@@ -26,6 +26,7 @@     Imm.Database.JsonFile     Imm.Feed     Imm.Hooks+    Imm.Hooks.Dummy     Imm.Hooks.SendMail     Imm.Hooks.WriteFile     Imm.HTTP@@ -34,7 +35,7 @@     Imm.Logger.Simple     Imm.Prelude     Imm.XML-    Imm.XML.Simple+    Imm.XML.Conduit   other-modules:     Imm.Aeson     Imm.Dyre@@ -42,13 +43,61 @@     Imm.Options     Imm.Pretty     Paths_imm-  build-depends: aeson, ansi-wl-pprint, atom-conduit >= 0.4, base == 4.*, blaze-html, blaze-markup, bytestring, case-insensitive, chunked-data >= 0.3.0, comonad, conduit, conduit-combinators, connection, containers, directory >= 1.2.3.0, dyre, fast-logger, filepath, free, hashable, HaskellNet, HaskellNet-SSL >= 0.3.3.0, http-client >= 0.4.30, http-client-tls, http-types, mime-mail, monoid-subclasses, microlens, mono-traversable >= 1, network, opml-conduit >= 0.6, optparse-applicative, rainbow, rainbox, rss-conduit >= 0.4.1, safe-exceptions, tagged, text, transformers, time, timerep >= 2.0.0.0, tls, uri-bytestring, xml, xml-conduit >= 1.5, xml-types+  build-depends:+    aeson,+    atom-conduit >= 0.4,+    base == 4.*,+    blaze-html,+    blaze-markup,+    bytestring,+    case-insensitive,+    conduit,+    connection,+    containers,+    directory >= 1.2.3.0,+    dyre,+    fast-logger,+    filepath,+    hashable,+    HaskellNet,+    HaskellNet-SSL >= 0.3.3.0,+    http-client >= 0.4.30,+    http-client-tls,+    http-types,+    lifted-base,+    microlens,+    mime-mail,+    monad-time,+    monoid-subclasses,+    mono-traversable >= 1,+    mtl,+    network,+    opml-conduit >= 0.6,+    optparse-applicative,+    prettyprinter,+    prettyprinter-ansi-terminal,+    rss-conduit >= 0.4.1,+    safe-exceptions,+    stm,+    streaming-bytestring,+    streaming-with,+    streamly,+    text,+    transformers,+    transformers-base,+    time,+    timerep >= 2.0.0.0,+    tls,+    uri-bytestring,+    xml,+    xml-conduit >= 1.5,+    xml-types   -- Build-tools:   hs-source-dirs: src/lib   ghc-options: -Wall -fno-warn-unused-do-bind  executable imm-  build-depends: imm, base == 4.*, free+  build-depends: imm, base == 4.*   main-is: Executable.hs   hs-source-dirs: src/bin   ghc-options: -Wall -fno-warn-unused-do-bind -threaded
src/bin/Executable.hs view
@@ -1,31 +1,23 @@ {-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-} --module Executable where  -- {{{ Imports import           Imm import           Imm.Database.JsonFile-import qualified Imm.Hooks.WriteFile   as WriteFile+import           Imm.Hooks.Dummy import           Imm.HTTP.Simple import           Imm.Logger.Simple import           Imm.Prelude-import           Imm.XML.Simple+import           Imm.XML.Conduit -import           System.Exit+import           Control.Concurrent.MVar -- }}} --- mkDummyCoHooks :: (MonadIO m, MonadThrow m) => () -> CoHooksF m ()--- mkDummyCoHooks _ = CoHooksF coOnNewElement where---   coOnNewElement _ _ = do---     io $ putStrLn "No hook defined."---     throwM $ ExitFailure 1 - main :: IO () main = do   logger <- defaultLogger   manager <- defaultManager-  database <- defaultDatabase+  database <- defaultDatabase :: IO (MVar (JsonFileDatabase FeedTable)) -  -- imm (mkCoHttpClient, manager) (mkCoDatabase, database) (mkCoLogger, logger) (mkDummyCoHooks, ()) (mkCoXmlParser, defaultPreProcess)-  imm (mkCoHttpClient, manager) (mkCoDatabase, database) (mkCoLogger, logger) (WriteFile.mkCoHooks, WriteFile.defaultSettings "/home/koral/feeds") (mkCoXmlParser, defaultPreProcess)+  imm $ mkModulesM manager database logger DummyHooks defaultXmlParser
src/lib/Imm/Aeson.hs view
@@ -1,6 +1,5 @@ {-# LANGUAGE FlexibleContexts  #-} {-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-} module Imm.Aeson where  -- {{{ Imports@@ -11,7 +10,7 @@ import           URI.ByteString -- }}} -parseJsonURI :: (MonadPlus m) => Value -> m URI+parseJsonURI :: MonadPlus m => Value -> m URI parseJsonURI (String s) = either (const mzero) return $ parseURI laxURIParserOptions $ encodeUtf8 s parseJsonURI _          = mzero 
src/lib/Imm/Boot.hs view
@@ -1,12 +1,10 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE TupleSections #-}-{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE FlexibleContexts          #-}+{-# LANGUAGE FlexibleInstances         #-}+{-# LANGUAGE MultiParamTypeClasses     #-}+{-# LANGUAGE NoImplicitPrelude         #-}+{-# LANGUAGE OverloadedStrings         #-}+{-# LANGUAGE RankNTypes                #-} -- | -- = Getting started --@@ -18,38 +16,86 @@ -- -- Your personal configuration is located at @$XDG_CONFIG_HOME\/imm\/imm.hs@. ----- == Interpreter pattern------ The behavior of this program can be customized through the interpreter pattern, implemented using free monads (for the DSL part) and cofree comonads (for the interpreter part).+-- == @ReaderT@ pattern ----- The design is inspired from <http://dlaing.org/cofun/ Cofun with cofree monads>.-module Imm.Boot (imm) where+-- The behavior of this program can be customized through the @ReaderT@ pattern.+module Imm.Boot (imm, Modules(..), ModulesM, mkModulesM) where  -- {{{ Imports-import qualified Imm.Core as Core-import Imm.Database.FeedTable as Database-import Imm.Database as Database-import Imm.Dyre as Dyre-import Imm.Feed-import Imm.HTTP as HTTP-import Imm.Hooks-import Imm.Logger as Logger-import Imm.Options as Options hiding(logLevel)-import Imm.Prelude-import Imm.Pretty-import Imm.XML+import qualified Imm.Core                   as Core+import           Imm.Database               as Database+import           Imm.Database.FeedTable     as Database+import           Imm.Dyre                   as Dyre+import           Imm.Feed+import           Imm.Hooks+import           Imm.HTTP                   as HTTP+import           Imm.Logger                 as Logger+import           Imm.Options                as Options hiding (logLevel)+import           Imm.Prelude+import           Imm.Pretty+import           Imm.XML -import Control.Comonad.Cofree-import Control.Monad.Trans.Free+import           Control.Monad.Time+import           Control.Monad.Trans.Reader+import           Data.Conduit.Combinators   (stdin)+import           Streamly                   (MonadAsync)+import           System.IO                  (hFlush)+-- }}} -import Data.Functor.Product-import Data.Functor.Sum+-- | Modules are independent features of the program which behavior can be controlled by the user.+data Modules httpClient databaseClient logger hooks xmlParser = Modules+  { _httpClient     :: httpClient      -- ^ HTTP client interpreter (cf "Imm.HTTP")+  , _databaseClient :: databaseClient  -- ^ Database interpreter (cf "Imm.Database")+  , _logger         :: logger          -- ^ Logging interpreter (cf "Imm.Logger")+  , _hooks          :: hooks           -- ^ Hooks interpreter (cf "Imm.Hooks")+  , _xmlParser      :: xmlParser       -- ^ XML parsing interpreter (cf "Imm.XML")+  } -import System.IO (hFlush)--- }}}+-- | Type-erased version of 'Modules', using existential quantification.+data ModulesM m = forall a b c d e .+  ( MonadHttpClient (ReaderT a m)+  , MonadDatabase FeedTable (ReaderT b m)+  , MonadLog (ReaderT c m)+  , MonadImm (ReaderT d m)+  , MonadXmlParser (ReaderT e m)+  ) => ModulesM (Modules a b c d e) +-- | Constructor for 'ModulesM'.+mkModulesM :: (MonadXmlParser (ReaderT e m), MonadImm (ReaderT d m), MonadLog (ReaderT c m), MonadDatabase FeedTable (ReaderT b m), MonadHttpClient (ReaderT a m))+           => a -> b -> c -> d -> e -> ModulesM m+mkModulesM a b c d e = ModulesM $ Modules a b c d e+++instance (MonadIO m, MonadLog (ReaderT c m)) => MonadLog (ReaderT (Modules a b c d e) m) where+  log l t = withReaderT _logger $ log l t+  getLogLevel = withReaderT _logger getLogLevel+  setLogLevel l = withReaderT _logger $ setLogLevel l+  setColorizeLogs c = withReaderT _logger $ setColorizeLogs c+  flushLogs = withReaderT _logger flushLogs++instance (Monad m, MonadImm (ReaderT d m)) => MonadImm (ReaderT (Modules a b c d e) m) where+  processNewElement feed element = withReaderT _hooks $ processNewElement feed element++instance (MonadThrow m, MonadHttpClient (ReaderT a m)) => MonadHttpClient (ReaderT (Modules a b c d e) m) where+  httpGet uri = withReaderT _httpClient $ httpGet uri++instance (MonadThrow m, MonadXmlParser (ReaderT e m))+  => MonadXmlParser (ReaderT (Modules a b c d e) m) where+  parseXml uri bytes = withReaderT _xmlParser $ parseXml uri bytes++instance (MonadThrow m, MonadDatabase FeedTable (ReaderT b m))+  => MonadDatabase FeedTable (ReaderT (Modules a b c d e) m) where+  _describeDatabase t = withReaderT _databaseClient $ _describeDatabase t+  _fetchList t k = withReaderT _databaseClient $ _fetchList t k+  _fetchAll t = withReaderT _databaseClient $ _fetchAll t+  _update t key f = withReaderT _databaseClient $ _update t key f+  _insertList t list = withReaderT _databaseClient $ _insertList t list+  _deleteList t k = withReaderT _databaseClient $ _deleteList t k+  _purge t = withReaderT _databaseClient $ _purge t+  _commit t = withReaderT _databaseClient $ _commit t++ -- | Main function, meant to be used in your personal configuration file.--- Each argument is an interpreter functor along with an initial state. -- -- Here is an example: --@@ -57,7 +103,7 @@ -- > import           Imm.Database.JsonFile -- > import           Imm.Feed -- > import           Imm.Hooks.SendMail--- > import           Imm.HTTP.Simple+-- > import           Imm.HTTP.Conduit -- > import           Imm.Logger.Simple -- > import           Imm.XML.Simple -- >@@ -67,7 +113,7 @@ -- >   manager  <- defaultManager -- >   database <- defaultDatabase -- >--- >   imm (mkCoHttpClient, manager) (mkCoDatabase, database) (mkCoLogger, logger) (mkCoHooks, sendmail) (mkCoXmlParser, defaultPreProcess)+-- >   imm $ mkModulesM manager database logger sendmail defaultXmlParser -- > -- > sendmail :: SendMailSettings -- > sendmail = SendMailSettings smtpServer formatMail@@ -83,32 +129,27 @@ -- > smtpServer _ _ = SMTPServer -- >   (Just $ Authentication PLAIN "user" "password") -- >   (StartTls "smtp.host" defaultSettingsSMTPSTARTTLS)-imm :: (a -> CoHttpClientF IO a, a)  -- ^ HTTP client interpreter (cf "Imm.HTTP")-    -> (b -> CoDatabaseF' IO b, b)   -- ^ Database interpreter (cf "Imm.Database")-    -> (c -> CoLoggerF IO c, c)      -- ^ Logger interpreter (cf "Imm.Logger")-    -> (d -> CoHooksF IO d, d)       -- ^ Hooks interpreter (cf "Imm.Hooks")-    -> (e -> CoXmlParserF IO e, e)   -- ^ XML parsing interpreter (cf "Imm.XML")-    -> IO ()-imm coHttpClient coDatabase coLogger coHooks coXmlParser = void $ do+imm :: ModulesM IO -> IO ()+imm modules = void $ do   options <- parseOptions-  Dyre.wrap (optionDyreMode options) realMain (optionCommand options, optionLogLevel options, optionColorizeLogs options, coiter next start)-  where (next, start) = mkCoImm coHttpClient coDatabase coLogger coHooks coXmlParser+  Dyre.wrap (optionDyreMode options) realMain (optionCommand options, optionLogLevel options, optionColorizeLogs options, modules) -realMain :: (MonadIO m, PairingM (CoImmF m) ImmF m, MonadCatch m)-         => (Command, LogLevel, Bool, Cofree (CoImmF m) a) -> m ()-realMain (command, logLevel, colorizeLogs, interpreter) = void $ interpret (\_ b -> return b) interpreter $ do-  setColorizeLogs colorizeLogs+realMain :: (MonadAsync m, MonadTime m, MonadCatch m)+         => (Command, LogLevel, Bool, ModulesM m) -> m ()+realMain (command, logLevel, enableColors, ModulesM modules) = void $ flip runReaderT modules $ do+  setColorizeLogs enableColors   setLogLevel logLevel   logDebug . ("Dynamic reconfiguration settings:" <++>) . indent 2 =<< Dyre.describePaths   logDebug $ "Executing: " <> pretty command-  logDebug . ("Using database:" <++>) . indent 2 =<< describeDatabase FeedTable+  logDebug . ("Using database:" <++>) . indent 2 =<< _describeDatabase FeedTable -  handleAny (logError . textual . displayException) $ case command of-    Check t        -> Core.check            =<< resolveTarget ByPassConfirmation t-    Import         -> Core.importOPML-    Read t         -> mapM_ Database.markAsRead   =<< resolveTarget AskConfirmation t-    Run t          -> Core.run              =<< resolveTarget ByPassConfirmation t-    Show t         -> Core.showFeed         =<< resolveTarget ByPassConfirmation t+  handleAny (logError . pretty . displayException) $ case command of+    Check t        -> Core.check =<< resolveTarget ByPassConfirmation t+    Help           -> liftBase $ putStrLn helpString+    Import         -> Core.importOPML stdin+    Read t         -> mapM_ Database.markAsRead =<< resolveTarget AskConfirmation t+    Run t          -> Core.run =<< resolveTarget ByPassConfirmation t+    Show t         -> Core.showFeed =<< resolveTarget ByPassConfirmation t     ShowVersion    -> Core.printVersions     Subscribe u c  -> Core.subscribe u c     Unread t       -> mapM_ Database.markAsUnread =<< resolveTarget AskConfirmation t@@ -118,26 +159,7 @@   Database.commit FeedTable   flushLogs --- * DSL/interpreter model -type CoImmF m = Product (CoHttpClientF m)-  (Product (CoDatabaseF' m)-   (Product (CoLoggerF m)-    (Product (CoHooksF m) (CoXmlParserF m)-    )))-type ImmF = Sum HttpClientF (Sum DatabaseF' (Sum LoggerF (Sum HooksF XmlParserF)))--mkCoImm :: (Functor m)-        => (a -> CoHttpClientF m a, a)-        -> (b -> CoDatabaseF' m b, b)-        -> (c -> CoLoggerF m c, c)-        -> (d -> CoHooksF m d, d)-        -> (e -> CoXmlParserF m e, e)-        -> ((a ::: b ::: c ::: d ::: e) -> CoImmF m (a ::: b ::: c ::: d ::: e), a ::: b ::: c ::: d ::: e)-mkCoImm (coHttpClient, a) (coDatabase, b) (coLogger, c) (coHooks, d) (coXmlParser, e) =-  (coHttpClient *: coDatabase *: coLogger *: coHooks *: coXmlParser, a +: b +: c +: d +: e)-- -- * Util  data SafeGuard = AskConfirmation | ByPassConfirmation@@ -147,19 +169,19 @@ instance Exception InterruptedException where   displayException _ = "Process interrupted" -promptConfirm :: (MonadIO m, MonadThrow m) => Text -> m ()+promptConfirm :: Text -> IO () promptConfirm s = do-  hPut stdout $ s <> " Confirm [Y/n] "-  io $ hFlush stdout+  putStr $ s <> " Confirm [Y/n] "+  hFlush stdout   x <- getLine   unless (null x || x == ("Y" :: Text)) $ throwM InterruptedException  -resolveTarget :: (MonadIO m, MonadThrow m, MonadFree f m, DatabaseF' :<: f)+resolveTarget :: (MonadBase IO m, MonadThrow m, MonadDatabase FeedTable m)               => SafeGuard -> Maybe Core.FeedRef -> m [FeedID] resolveTarget s Nothing = do   result <- keys <$> Database.fetchAll FeedTable-  when (s == AskConfirmation) . promptConfirm $ "This will affect " <> show (length result) <> " feeds."+  when (s == AskConfirmation) $ liftBase $ promptConfirm $ "This will affect " <> show (length result) <> " feeds."   return result resolveTarget _ (Just (ByUID i)) = do   result <- fst . (!! i) . mapToList <$> Database.fetchAll FeedTable
src/lib/Imm/Core.hs view
@@ -1,111 +1,111 @@-{-# LANGUAGE FlexibleContexts       #-}-{-# LANGUAGE NoImplicitPrelude      #-}-{-# LANGUAGE OverloadedStrings      #-}-{-# LANGUAGE ScopedTypeVariables    #-}-{-# LANGUAGE TupleSections          #-}-{-# LANGUAGE PartialTypeSignatures  #-}-{-# LANGUAGE TypeFamilies           #-}-{-# LANGUAGE TypeOperators          #-}+{-# LANGUAGE FlexibleContexts      #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE NoImplicitPrelude     #-}+{-# LANGUAGE OverloadedStrings     #-}+{-# LANGUAGE PartialTypeSignatures #-}+{-# LANGUAGE TupleSections         #-}+{-# LANGUAGE TypeFamilies          #-} module Imm.Core ( -- * Types-    FeedRef,+  FeedRef, -- * Actions-    printVersions,-    subscribe,-    showFeed,-    check,-    run,-    importOPML,+  printVersions,+  subscribe,+  showFeed,+  check,+  run,+  importOPML, ) where  -- {{{ Imports-import qualified Imm.Database.FeedTable as Database-import Imm.Database.FeedTable hiding(markAsRead, markAsUnread)-import qualified Imm.Database as Database-import Imm.Feed-import Imm.Hooks as Hooks-import Imm.HTTP (HttpClientF)-import qualified Imm.HTTP as HTTP-import Imm.Logger-import Imm.Prelude-import Imm.Pretty-import Imm.XML---- import Control.Concurrent.Async.Lifted (Async, async, mapConcurrently, waitAny)--- import Control.Concurrent.Async.Pool-import Control.Monad.Free+import           Imm.Database                (MonadDatabase)+import qualified Imm.Database                as Database+import           Imm.Database.FeedTable+import qualified Imm.Database.FeedTable      as Database+import           Imm.Feed+import           Imm.Hooks                   as Hooks+import           Imm.HTTP                    (MonadHttpClient)+import qualified Imm.HTTP                    as HTTP+import           Imm.Logger+import           Imm.Prelude+import           Imm.Pretty+import           Imm.XML -import qualified Data.ByteString              as ByteString-import Data.Conduit-import Data.Conduit.Combinators as Conduit (stdin)-import qualified Data.Map as Map-import Data.NonNull-import Data.Set (Set)-import Data.Time.Format-import Data.Tree+import           Control.Concurrent.STM      (STM, atomically)+import           Control.Concurrent.STM.TVar+import           Control.Monad.Time+import           Data.Conduit+import qualified Data.Map                    as Map+import           Data.NonNull+import           Data.Set                    (Set)+import qualified Data.Set                    as Set+import qualified Data.Text                   as Text+import           Data.Tree import           Data.Version--import qualified Paths_imm                      as Package--import Rainbow (chunksToByteStrings, toByteStringsColors256, chunk)-import Rainbox-+import qualified Paths_imm                   as Package+import           Streamly                    hiding ((<>))+import qualified Streamly.Prelude            as Stream import           System.Info--import Text.OPML.Conduit.Parse-import Text.OPML.Types as OPML-import Text.XML as XML ()-import Text.XML.Stream.Parse as XML-+import           Text.OPML.Conduit.Parse+import           Text.OPML.Types             as OPML+import           Text.XML                    as XML ()+import           Text.XML.Stream.Parse       as XML import           URI.ByteString -- }}}  -printVersions :: (MonadIO m) => m ()-printVersions = io $ do-  putStrLn $ "imm-" ++ showVersion Package.version-  putStrLn $ "compiled by " ++ compilerName ++ "-" ++ showVersion compilerVersion+printVersions :: (MonadBase IO m) => m ()+printVersions = liftBase $ do+  putStrLn $ "imm-" <> Text.pack (showVersion Package.version)+  putStrLn $ "compiled by " <> Text.pack compilerName <> "-" <> Text.pack (showVersion compilerVersion)  -- | Print database status for given feed(s)-showFeed :: (MonadIO m, LoggerF :<: f, MonadThrow m, MonadFree f m, DatabaseF' :<: f)+showFeed :: (MonadLog m, MonadThrow m, MonadDatabase FeedTable m)          => [FeedID] -> m () showFeed feedIDs = do-  feeds <- Database.fetchList FeedTable feedIDs+  entries <- Database.fetchList FeedTable feedIDs   flushLogs-  if null feeds then logWarning "No subscription" else putBox $ entryTableToBox feeds+  when (null entries) $ logWarning "No subscription"+  forM_ (zip [1..] $ Map.elems entries) $ \(i, entry) ->+    logInfo $ pretty (i :: Int) <+> prettyDatabaseEntry entry  -- | Register the given feed URI in database-subscribe :: (LoggerF :<: f, MonadFree f m, DatabaseF' :<: f, MonadCatch m)-          => URI -> Maybe Text -> m ()-subscribe uri category = Database.register (FeedID uri) $ fromMaybe "default" category+subscribe :: (MonadLog m, MonadDatabase FeedTable m, MonadCatch m)+          => URI -> Set Text -> m ()+subscribe uri = Database.register (FeedID uri)  -- | Check for unread elements without processing them-check :: (MonadIO m, MonadCatch m, LoggerF :<: f, MonadFree f m, DatabaseF' :<: f, HttpClientF :<: f, XmlParserF :<: f)+check :: (MonadAsync m, MonadCatch m, MonadLog m, MonadDatabase FeedTable m, MonadHttpClient m, MonadXmlParser m)       => [FeedID] -> m () check feedIDs = do-  results <- for (zip ([1..] :: [Int]) feedIDs) $ \(i, feedID) -> do-    logInfo $ brackets (fill width (bold $ cyan $ pretty i) <+> "/" <+> pretty total) <+> "Checking" <+> magenta (pretty feedID) <> "..."-    try $ checkOne feedID+  progress <- liftBase $ newTVarIO 0 -  flushLogs+  results <- Stream.toList $ wAsyncly $ do+    feedID <- Stream.fromFoldable feedIDs+    result <- lift $ tryAny $ checkOne feedID+    let logResult = either (red . pretty . displayException) (\n -> green (pretty n) <+> "new element(s)") result+    n <- liftBase $ atomically $ do+      modifyTVar (progress :: TVar Int) (+ 1)+      readTVar progress+    lift $ logInfo $ brackets (fill width (bold $ cyan $ pretty n) <+> "/" <+> pretty total) <+> "Checked" <+> magenta (pretty feedID) <+> "=>" <+> logResult+    return result -  putBox $ statusTableToBox $ mapFromList $ zip feedIDs results+  flushLogs    let (failures, successes) = partitionEithers $ zipWith (\a -> bimap (a,) (a,)) feedIDs results   unless (null failures) $ logError $ bold (pretty $ length failures) <+> "feeds in error"-  forM_ failures $ \(feedID, e) ->-    logError $ indent 2 (pretty feedID <++> indent 2 (pretty $ displayException e))+  logInfo $ bold (pretty $ sum $ map snd successes) <+> "new element(s) overall"    where width = length (show total :: String)         total = length feedIDs -checkOne :: (MonadIO m, MonadCatch m, LoggerF :<: f, MonadFree f m, DatabaseF' :<: f, HttpClientF :<: f, XmlParserF :<: f)+checkOne :: (MonadBase IO m, MonadCatch m, MonadLog m, MonadDatabase FeedTable m, MonadHttpClient m, MonadXmlParser m)          => FeedID -> m Int checkOne feedID = do   feed <- getFeed feedID   case feed of     Atom _ -> logDebug $ "Parsed Atom feed: " <> pretty feedID-    Rss _ -> logDebug $ "Parsed RSS feed: " <> pretty feedID+    Rss _  -> logDebug $ "Parsed RSS feed: " <> pretty feedID    let dates = mapMaybe getDate $ getElements feed @@ -117,12 +117,19 @@         unread _ _                = True  -run :: (MonadIO m, MonadCatch m, HooksF :<: f, LoggerF :<: f, MonadFree f m, DatabaseF' :<: f, HttpClientF :<: f, XmlParserF :<: f)+run :: (MonadTime m, MonadAsync m, MonadCatch m, MonadImm m, MonadLog m, MonadDatabase FeedTable m, MonadHttpClient m, MonadXmlParser m)     => [FeedID] -> m () run feedIDs = do-  results <- for (zip ([1..] :: [Int]) feedIDs) $ \(i, feedID) -> do-    logInfo $ brackets (fill width (bold $ cyan $ pretty i) <+> "/" <+> pretty total) <+> "Processing" <+> magenta (pretty feedID) <> "..."-    result <- tryAny $ runOne feedID+  progress <- liftBase $ newTVarIO 0++  results <- Stream.toList $ wAsyncly $ do+    feedID <- Stream.fromFoldable feedIDs+    result <- lift $ tryAny $ runOne feedID+    let logResult = either (red . pretty . displayException) (\n -> green (pretty n) <+> "new element(s)") result+    n <- liftBase $ atomically $ do+      modifyTVar progress (+ 1)+      readTVar progress :: STM Int+    lift $ logInfo $ brackets (fill width (bold $ cyan $ pretty n) <+> "/" <+> pretty total) <+> "Processed" <+> magenta (pretty feedID) <+> "=>" <+> logResult     return $ bimap (feedID,) (feedID,) result    flushLogs@@ -130,82 +137,49 @@   let (failures, successes) = partitionEithers results    unless (null failures) $ logError $ bold (pretty $ length failures) <+> "feeds in error"-  forM_ failures $ \(feedID, e) ->-    logError $ indent 2 (pretty feedID <++> indent 2 (pretty $ displayException e))+  logInfo $ bold (pretty $ sum $ map snd successes) <+> "new element(s) overall"    where width = length (show total :: String)         total = length feedIDs -runOne :: (MonadIO m, MonadCatch m, HooksF :<: f, LoggerF :<: f, MonadFree f m, DatabaseF' :<: f, HttpClientF :<: f, XmlParserF :<: f)-    => FeedID -> m ()+runOne :: (MonadTime m, MonadCatch m, MonadImm m, MonadLog m, MonadDatabase FeedTable m, MonadHttpClient m, MonadXmlParser m)+       => FeedID -> m Int runOne feedID = do   feed <- getFeed feedID   unreadElements <- filterM (fmap not . isRead feedID) $ getElements feed -  unless (null unreadElements) $ logInfo $ indent 2 $ green (pretty $ length unreadElements) <+> "new element(s)"-   forM_ unreadElements $ \element -> do     onNewElement feed element     mapM_ (Database.addReadHash feedID) $ getHashes element    Database.markAsRead feedID+  return $ length unreadElements  -isRead :: (MonadCatch m, DatabaseF' :<: f, MonadFree f m) => FeedID -> FeedElement -> m Bool+isRead :: (MonadCatch m, MonadDatabase FeedTable m) => FeedID -> FeedElement -> m Bool isRead feedID element = do   DatabaseEntry _ _ readHashes lastCheck <- Database.fetch FeedTable feedID   let matchHash = not $ null $ (setFromList (getHashes element) :: Set Int) `intersection` readHashes       matchDate = case (lastCheck, getDate element) of-        (Nothing, _) -> False-        (_, Nothing) -> False+        (Nothing, _)     -> False+        (_, Nothing)     -> False         (Just a, Just b) -> a > b   return $ matchHash || matchDate --- | 'subscribe' to all feeds described by the OPML document provided in input (stdin)-importOPML :: (MonadIO m, LoggerF :<: f, MonadFree f m, DatabaseF' :<: f, MonadCatch m) => m ()-importOPML = do-  opml <- runConduit $ Conduit.stdin =$= XML.parseBytes def =$= force "Invalid OPML" parseOpml+-- | 'subscribe' to all feeds described by the OPML document provided in input+importOPML :: (MonadLog m, MonadDatabase FeedTable m, MonadCatch m)+           => ConduitT () ByteString m () -> m ()+importOPML input = do+  opml <- runConduit $ input .| XML.parseBytes def .| force "Invalid OPML" parseOpml   forM_ (opmlOutlines opml) $ importOPML' mempty -importOPML' :: (MonadIO m, LoggerF :<: f, MonadFree f m, DatabaseF' :<: f, MonadCatch m)-            => Maybe Text -> Tree OpmlOutline -> m ()-importOPML' _ (Node (OpmlOutlineGeneric b _) sub) = mapM_ (importOPML' (Just . toNullable $ OPML.text b)) sub+importOPML' :: (MonadLog m, MonadDatabase FeedTable m, MonadCatch m)+            => Set Text -> Tree OpmlOutline -> m ()+importOPML' _ (Node (OpmlOutlineGeneric b _) sub) = mapM_ (importOPML' (Set.singleton . toNullable $ OPML.text b)) sub importOPML' c (Node (OpmlOutlineSubscription _ s) _) = subscribe (xmlUri s) c importOPML' _ _ = return ()  -getFeed :: (MonadIO m, MonadCatch m, MonadFree f m, HttpClientF :<: f, LoggerF :<: f, XmlParserF :<: f)+getFeed :: (MonadCatch m, MonadHttpClient m, MonadLog m, MonadXmlParser m)         => FeedID -> m Feed getFeed (FeedID uri) = HTTP.get uri >>= parseXml uri----- * Boxes--putBox :: (Orientation a, MonadIO m) => Box a -> m ()-putBox = io . mapM_ ByteString.putStr . chunksToByteStrings toByteStringsColors256 . toList . render--cell :: Text -> Cell-cell a = Cell (singleton $ singleton $ chunk a) top left mempty--type EntryTable = Map FeedID DatabaseEntry--entryTableToBox :: EntryTable -> Box Horizontal-entryTableToBox t = tableByColumns $ Rainbox.intersperse sep $ fromList [col1, col2, col3, col4] where-  result = sortBy (comparing entryURI) $ map snd $ mapToList t-  col1 = fromList $ cell "UID" : map (cell . show) [0..(length result - 1)]-  col2 = fromList $ cell "CATEGORY" : map (cell . entryCategory) result-  col3 = fromList $ cell "LAST CHECK" : map (cell . format . entryLastCheck) result-  col4 = fromList $ cell "FEED URI" : map (cell . show . prettyURI . entryURI) result-  format = maybe "<never>" (fromString . formatTime defaultTimeLocale "%F %R")-  sep = fromList [separator mempty 1]---type StatusTable = Map FeedID (Either SomeException Int)--statusTableToBox :: StatusTable -> Box Horizontal-statusTableToBox t = tableByColumns $ Rainbox.intersperse sep $ fromList [col1, col2, col3] where-  result = sortBy (comparing fst) $ Map.toList t-  col1 = fromList $ cell "# UNREAD" : map (cell . either (const "?") show . snd) result-  col2 = fromList $ cell "STATUS" : map (cell . either (const "ERROR") (const "OK") . snd) result-  col3 = fromList $ cell "FEED" : map (cell . show . pretty . fst) result-  sep = fromList [separator mempty 2]
src/lib/Imm/Database.hs view
@@ -1,89 +1,42 @@-{-# LANGUAGE DeriveFunctor         #-} {-# LANGUAGE FlexibleContexts      #-} {-# LANGUAGE FlexibleInstances     #-} {-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE NamedFieldPuns        #-} {-# LANGUAGE NoImplicitPrelude     #-} {-# LANGUAGE OverloadedStrings     #-} {-# LANGUAGE StandaloneDeriving    #-} {-# LANGUAGE TypeFamilies          #-}-{-# LANGUAGE TypeOperators         #-} {-# LANGUAGE UndecidableInstances  #-}--- | DSL/interpreter model for a generic key-value database+-- | Database module abstracts over a key-value database that supports CRUD operations. module Imm.Database where  -- {{{ Imports-import           Imm.Error import           Imm.Logger import           Imm.Prelude--import           Control.Monad.Trans.Free+import           Imm.Pretty -import           Text.PrettyPrint.ANSI.Leijen hiding ((<$>), (<>))+import           Data.Map    (Map) -- }}} --- * DSL/interpreter+-- * Types  -- | Generic database table-class (Ord (Key t), Show (Key t), Show (Entry t), Typeable t, Show t, Pretty t, Pretty (Key t), Pretty (Entry t))+class (Ord (Key t), Show (Key t), Show (Entry t), Typeable t, Show t, Pretty t, Pretty (Key t))   => Table t where   type Key t :: *   type Entry t :: * --- | Database DSL-data DatabaseF t next-  = Describe t (Doc -> next)-  | FetchList t [Key t] (Either SomeException (Map (Key t) (Entry t)) -> next)-  | FetchAll t (Either SomeException (Map (Key t) (Entry t)) -> next)-  | Update t (Key t) (Entry t -> Entry t) (Either SomeException () -> next)-  | InsertList t [(Key t, Entry t)] (Either SomeException () -> next)-  | DeleteList t [Key t] (Either SomeException () -> next)-  | Purge t (Either SomeException () -> next)-  | Commit t (Either SomeException () -> next)-  deriving(Functor)---- | Database interpreter-data CoDatabaseF t m a = CoDatabaseF-  { describeH   :: m (Doc, a)-  , fetchListH  :: [Key t] -> m (Either SomeException (Map (Key t) (Entry t)), a)-  , fetchAllH   :: m (Either SomeException (Map (Key t) (Entry t)), a)-  , updateH     :: Key t -> (Entry t -> Entry t) -> m (Either SomeException (), a)-  , insertListH :: [(Key t, Entry t)] -> m (Either SomeException (), a)-  , deleteListH :: [Key t] -> m (Either SomeException (), a)-  , purgeH      :: m (Either SomeException (), a)-  , commitH     :: m (Either SomeException (), a)-  } deriving(Functor)--instance Monad m => PairingM (CoDatabaseF t m) (DatabaseF t) m where-  -- pairM :: (a -> b -> m r) -> f a -> g b -> m r-  pairM p CoDatabaseF{describeH} (Describe _ next) = do-    (result, a) <- describeH-    p a $ next result-  pairM p CoDatabaseF{fetchListH} (FetchList _ key next) = do-    (result, a) <- fetchListH key-    p a $ next result-  pairM p CoDatabaseF{fetchAllH} (FetchAll _ next) = do-    (result, a) <- fetchAllH-    p a $ next result-  pairM p CoDatabaseF{updateH} (Update _ key f next) = do-    (result, a) <- updateH key f-    p a $ next result-  pairM p CoDatabaseF{insertListH} (InsertList _ rows next) = do-    (result, a) <- insertListH rows-    p a $ next result-  pairM p CoDatabaseF{deleteListH} (DeleteList _ k next) = do-    (result, a) <- deleteListH k-    p a $ next result-  pairM p CoDatabaseF{purgeH} (Purge _ next) = do-    (result, a) <- purgeH-    p a $ next result-  pairM p CoDatabaseF{commitH} (Commit _ next) = do-    (result, a) <- commitH-    p a $ next result+-- | Monad capable of interacting with a key-value store.+class MonadThrow m => MonadDatabase t m where+  _describeDatabase :: t -> m (Doc a)+  _fetchList :: t -> [Key t] -> m (Map (Key t) (Entry t))+  _fetchAll :: t -> m (Map (Key t) (Entry t))+  _update :: t -> Key t -> (Entry t -> Entry t) -> m ()+  _insertList :: t -> [(Key t, Entry t)] -> m ()+  _deleteList :: t -> [Key t] -> m ()+  _purge :: t -> m ()+  _commit :: t -> m ()  --- * Exception- data DatabaseException t   = NotCommitted t   | NotDeleted t [Key t]@@ -100,75 +53,54 @@   displayException = show . pretty  instance (Pretty t, Pretty (Key t)) => Pretty (DatabaseException t) where-  pretty (NotCommitted _) = text "Unable to commit database changes."-  pretty (NotDeleted _ x) = text "Unable to delete the following entries in database:" <++> indent 2 (vsep $ map pretty x)-  pretty (NotFound _ x) = text "Unable to find the following entries in database:" <++> indent 2 (vsep $ map pretty x)-  pretty (NotInserted _ x) = text "Unable to insert the following entries in database:" <++> indent 2 (vsep $ map (pretty . fst) x)-  pretty (NotPurged t) = text "Unable to purge database" <+> pretty t-  pretty (NotUpdated _ x) = text "Unable to update the following entry in database:" <++> indent 2 (pretty x)-  pretty (UnableFetchAll _) = text "Unable to fetch all entries from database."+  pretty (NotCommitted _) = "Unable to commit database changes."+  pretty (NotDeleted _ x) = "Unable to delete the following entries in database:" <++> indent 2 (vsep $ map pretty x)+  pretty (NotFound _ x) = "Unable to find the following entries in database:" <++> indent 2 (vsep $ map pretty x)+  pretty (NotInserted _ x) = "Unable to insert the following entries in database:" <++> indent 2 (vsep $ map (pretty . fst) x)+  pretty (NotPurged t) = "Unable to purge database" <+> pretty t+  pretty (NotUpdated _ x) = "Unable to update the following entry in database:" <++> indent 2 (pretty x)+  pretty (UnableFetchAll _) = "Unable to fetch all entries from database."   -- * Primitives -describeDatabase :: (MonadFree f m, DatabaseF t :<: f)-                 => t -> m Doc-describeDatabase t = liftF . inj $ Describe t id--fetch :: (MonadFree f m, DatabaseF t :<: f, Table t, MonadThrow m)-      => t -> Key t -> m (Entry t)+fetch :: (MonadDatabase t m, Table t, MonadThrow m) => t -> Key t -> m (Entry t) fetch t k = do-  results <- liftF . inj $ FetchList t [k] id-  result <- lookup k <$> liftE results-  maybe (throwM $ NotFound t [k]) return result+  results <- _fetchList t [k]+  maybe (throwM $ NotFound t [k]) return $ lookup k results -fetchList :: (MonadFree f m, DatabaseF t :<: f, MonadThrow m)-          => t -> [Key t] -> m (Map (Key t) (Entry t))-fetchList t k = do-  result <- liftF . inj $ FetchList t k id-  liftE result+fetchList :: (MonadDatabase t m, MonadThrow m) => t -> [Key t] -> m (Map (Key t) (Entry t))+fetchList = _fetchList -fetchAll :: (MonadThrow m, MonadFree f m, DatabaseF t :<: f) => t -> m (Map (Key t) (Entry t))-fetchAll t = do-  result <- liftF . inj $ FetchAll t id-  liftE result+fetchAll :: (MonadThrow m, MonadDatabase t m) => t -> m (Map (Key t) (Entry t))+fetchAll = _fetchAll -update :: (MonadFree f m, DatabaseF t :<: f, MonadThrow m)-       => t -> Key t -> (Entry t -> Entry t) -> m ()-update t k f = do-  result <- liftF . inj $ Update t k f id-  liftE result+update :: (MonadDatabase t m, MonadThrow m) => t -> Key t -> (Entry t -> Entry t) -> m ()+update  = _update -insert :: (MonadThrow m, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f)-       => t -> Key t -> Entry t -> m ()+insert :: (MonadThrow m, MonadDatabase t m, MonadLog m) => t -> Key t -> Entry t -> m () insert t k v = insertList t [(k, v)] -insertList :: (MonadThrow m, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f)-           => t -> [(Key t, Entry t)] -> m ()+insertList :: (MonadThrow m, MonadDatabase t m, MonadLog m) => t -> [(Key t, Entry t)] -> m () insertList t i = do   logInfo $ "Inserting " <> yellow (pretty $ length i) <> " entries..."-  result <- liftF . inj $ InsertList t i id-  liftE result+  _insertList t i -delete :: (MonadThrow m, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f) => t -> Key t -> m ()+delete :: (MonadThrow m, MonadDatabase t m, MonadLog m) => t -> Key t -> m () delete t k = deleteList t [k] -deleteList :: (MonadThrow m, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f)-           => t -> [Key t] -> m ()+deleteList :: (MonadThrow m, MonadDatabase t m, MonadLog m) => t -> [Key t] -> m () deleteList t k = do   logInfo $ "Deleting " <> yellow (pretty $ length k) <> " entries..."-  result <- liftF . inj $ DeleteList t k id-  liftE result+  _deleteList t k -purge :: (MonadThrow m, MonadFree f m, DatabaseF t :<: f, LoggerF :<: f) => t -> m ()+purge :: (MonadThrow m, MonadDatabase t m, MonadLog m) => t -> m () purge t = do   logInfo "Purging database..."-  result <- liftF . inj $ Purge t id-  liftE result+  _purge t -commit :: (MonadThrow m, MonadFree f m, DatabaseF t :<: f, LoggerF :<: f) => t -> m ()+commit :: (MonadThrow m, MonadDatabase t m, MonadLog m) => t -> m () commit t = do   logDebug "Committing database transaction..."-  result <- liftF . inj $ Commit t id-  liftE result+  _commit t   logDebug "Database transaction committed"
src/lib/Imm/Database/FeedTable.hs view
@@ -1,25 +1,22 @@-{-# LANGUAGE FlexibleContexts      #-}-{-# LANGUAGE NoImplicitPrelude     #-}-{-# LANGUAGE OverloadedStrings     #-}-{-# LANGUAGE TypeFamilies     #-}-{-# LANGUAGE TypeOperators     #-}+{-# LANGUAGE FlexibleContexts  #-}+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeFamilies      #-} -- | Feed table definitions. This is a specialization of "Imm.Database". module Imm.Database.FeedTable where  -- {{{ Imports-import Imm.Aeson-import Imm.Database+import           Imm.Aeson+import           Imm.Database import           Imm.Logger-import Imm.Prelude-import Imm.Pretty--import Control.Monad.Trans.Free+import           Imm.Prelude+import           Imm.Pretty +import           Control.Monad.Time import           Data.Aeson-import           Data.Set (Set)-import           Data.Time           as Time--import URI.ByteString+import           Data.Set           (Set)+import           Data.Time+import           URI.ByteString -- }}}  -- * Types@@ -28,6 +25,9 @@ newtype FeedID = FeedID URI   deriving(Eq, Ord, Show) +prettyFeedID :: FeedID -> Doc AnsiStyle+prettyFeedID (FeedID uri) = prettyURI uri+ instance FromJSON FeedID where   parseJSON = fmap FeedID . parseJsonURI @@ -39,33 +39,36 @@   data DatabaseEntry = DatabaseEntry-  { entryURI         :: URI-  , entryCategory    :: Text-  , entryReadHashes  :: Set Int-  , entryLastCheck   :: Maybe UTCTime+  { entryURI        :: URI+  , entryTags       :: Set Text+  , entryReadHashes :: Set Int+  , entryLastCheck  :: Maybe UTCTime   } deriving(Eq, Show) -instance Pretty DatabaseEntry where-  pretty r = text "Entry:" <+> prettyURI (entryURI r) <++> indent 2-    ( text "Category:" <+> text (fromText $ entryCategory r)-    <++> text "Last check:" <+> text (maybe "<never>" (formatTime defaultTimeLocale rfc822DateFormat) $ entryLastCheck r)-    <++> text "Read hashes:" <+> text (show $ length $ entryReadHashes r)-    )+prettyDatabaseEntry :: DatabaseEntry -> Doc AnsiStyle+prettyDatabaseEntry entry = magenta feedID+  <++> indent 3 tags+  <++> indent 3 ("Last checked:" <+> lastCheck) +  where feedID = prettyURI $ entryURI entry+        tags = sep $ map ((<>) "#" . pretty) $ toList $ entryTags entry+        lastCheck = format $ entryLastCheck entry+        format = maybe "never" (fromString . formatTime defaultTimeLocale "%F %R")+ instance FromJSON DatabaseEntry where-  parseJSON (Object v) = DatabaseEntry <$> (parseJsonURI =<< v .: "uri") <*> v .: "category" <*> v.: "readHashes" <*> v .: "lastCheck"+  parseJSON (Object v) = DatabaseEntry <$> (parseJsonURI =<< v .: "uri") <*> v .: "tags" <*> v.: "readHashes" <*> v .: "lastCheck"   parseJSON _          = mzero  instance ToJSON DatabaseEntry where   toJSON entry = object     [ "uri"        .= toJsonURI (entryURI entry)-    , "category"   .= entryCategory entry+    , "tags"       .= entryTags entry     , "readHashes" .= entryReadHashes entry     , "lastCheck"  .= entryLastCheck entry     ] -newDatabaseEntry :: FeedID -> Text -> DatabaseEntry-newDatabaseEntry (FeedID uri) category = DatabaseEntry uri category mempty Nothing+newDatabaseEntry :: FeedID -> Set Text -> DatabaseEntry+newDatabaseEntry (FeedID uri) tags = DatabaseEntry uri tags mempty Nothing  -- | Singleton type to represent feeds table data FeedTable = FeedTable@@ -82,50 +85,47 @@ data FeedStatus = Unknown | New | LastUpdate UTCTime  instance Pretty FeedStatus where-  pretty Unknown        = text "Unknown"-  pretty New            = text "New"-  pretty (LastUpdate x) = text "Last update:" <+> text (formatTime defaultTimeLocale rfc822DateFormat x)+  pretty Unknown        = "Unknown"+  pretty New            = "New"+  pretty (LastUpdate x) = "Last update:" <+> pretty (formatTime defaultTimeLocale rfc822DateFormat x)  -data Database = Database [DatabaseEntry]+newtype Database = Database [DatabaseEntry]   deriving (Eq, Show) -type DatabaseF' = DatabaseF FeedTable-type CoDatabaseF' = CoDatabaseF FeedTable- -- * Primitives -register :: (MonadThrow m, LoggerF :<: f, DatabaseF' :<: f, MonadFree f m)-          => FeedID -> Text -> m ()-register feedID category = do-  logInfo $ "Registering feed " <> magenta (pretty feedID) <> "..."-  insert FeedTable feedID $ newDatabaseEntry feedID category+register :: (MonadThrow m, MonadLog m, MonadDatabase FeedTable m)+          => FeedID -> Set Text -> m ()+register feedID tags = do+  logInfo $ "Registering feed" <+> magenta (pretty feedID) <> "..."+  insert FeedTable feedID $ newDatabaseEntry feedID tags -getStatus :: (DatabaseF' :<: f, MonadFree f m, MonadCatch m)+getStatus :: (MonadDatabase FeedTable m, MonadCatch m)           => FeedID -> m FeedStatus getStatus feedID = handleAny (\_ -> return Unknown) $ do   result <- fmap Just (fetch FeedTable feedID) `catchAny` (\_ -> return Nothing)   return $ maybe New LastUpdate $ entryLastCheck =<< result -addReadHash :: (DatabaseF' :<: f, MonadFree f m, MonadThrow m, LoggerF :<: f)+addReadHash :: (MonadDatabase FeedTable m, MonadThrow m, MonadLog m)                => FeedID -> Int -> m () addReadHash feedID hash = do-  logDebug $ "Adding read hash: " <> pretty hash <> "..."+  logDebug $ "Adding read hash:" <+> pretty hash <> "..."   update FeedTable feedID f   where f a = a { entryReadHashes = insertSet hash $ entryReadHashes a }  -- | Set the last check time to now-markAsRead :: (MonadIO m, DatabaseF' :<: f, MonadFree f m, MonadThrow m, LoggerF :<: f)+markAsRead :: (MonadTime m, MonadDatabase FeedTable m, MonadThrow m, MonadLog m)            => FeedID -> m () markAsRead feedID = do-  logDebug $ "Marking feed as read: " <> pretty feedID <> "..."-  currentTime <- io Time.getCurrentTime-  update FeedTable feedID (f currentTime)+  logDebug $ "Marking feed as read:" <+> pretty feedID <> "..."+  utcTime <- currentTime+  update FeedTable feedID (f utcTime)   where f time a = a { entryLastCheck = Just time }  -- | Unset feed's last update and remove all read hashes-markAsUnread ::  (DatabaseF' :<: f, MonadFree f m, MonadThrow m, LoggerF :<: f)+markAsUnread :: (MonadDatabase FeedTable m, MonadThrow m, MonadLog m)              => FeedID -> m () markAsUnread feedID = do-  logInfo $ "Marking feed as unread: " <> show (pretty feedID) <> "..."+  logInfo $ "Marking feed as unread:" <+> prettyFeedID feedID <> "..."   update FeedTable feedID $ \a -> a { entryReadHashes = mempty, entryLastCheck = Nothing }
src/lib/Imm/Database/JsonFile.hs view
@@ -4,44 +4,54 @@ {-# 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+-- | Implementation of "Imm.Database" based on a JSON file.+module Imm.Database.JsonFile+  ( JsonFileDatabase+  , mkJsonFileDatabase+  , defaultDatabase+  , JsonException(..)+  , module Imm.Database.FeedTable+  ) where  -- {{{ Imports-import           Imm.Database           hiding (commit, delete, fetchAll,-                                         insert, purge, update)-import           Imm.Database.FeedTable as Reexport+import           Imm.Database                   hiding (commit, delete, insert,+                                                 purge, update)+import           Imm.Database.FeedTable import           Imm.Error-import           Imm.Prelude            hiding (catch, delete, keys)+import           Imm.Prelude                    hiding (delete, keys)+import           Imm.Pretty +import           Control.Concurrent.MVar.Lifted+import           Control.Monad.Reader.Class+import           Control.Monad.Trans.Reader     (ReaderT) import           Data.Aeson-import qualified Data.Map               as Map-import qualified Data.Set               as Set-+import           Data.ByteString.Lazy           (hPut)+import           Data.ByteString.Streaming      (hGetContents, toLazy_)+import           Data.Map                       (Map)+import qualified Data.Map                       as Map+import qualified Data.Set                       as Set+import           Streaming.With 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+  pretty (JsonFileDatabase file _ _) = "JSON database: " <+> pretty 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"+defaultDatabase :: Table t => IO (MVar (JsonFileDatabase t))+defaultDatabase = do+  databaseFile <- getXdgDirectory XdgConfig "imm/feeds.json"+  newMVar $ mkJsonFileDatabase databaseFile   data JsonException = UnableDecode@@ -51,36 +61,35 @@   displayException _ = "Unable to parse JSON"  --- * Interpreter+instance (Table t, FromJSON (Key t), FromJSON (Entry t), ToJSON (Key t), ToJSON (Entry t))+  => MonadDatabase t (ReaderT (MVar (JsonFileDatabase t)) IO) where+  _describeDatabase _ = pretty <$> (readMVar =<< ask)+  _fetchList t keys = Map.filterWithKey (\uri _ -> member uri $ Set.fromList keys) <$> fetchAll t+  _fetchAll _ = do+    mvar <- ask+    lift $ modifyMVar mvar $ \database -> do+      a@(JsonFileDatabase _ cache _) <- loadInCache database+      return (a, cache)+  _update _ key f = exec (\a -> update a key f)+  _insertList _ rows = exec $ insert rows+  _deleteList _ keys = exec $ delete keys+  _purge _ = exec purge+  _commit _ = exec commit --- | 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+exec :: (a -> IO a) -> ReaderT (MVar a) IO ()+exec f = do+  mvar <- ask+  lift $ modifyMVar_ mvar f   -- * Low-level implementation -loadInCache :: (Table t, MonadIO m, MonadCatch m, FromJSON (Key t), FromJSON (Entry t))-            => JsonFileDatabase t -> m (JsonFileDatabase t)+loadInCache :: (Table t, FromJSON (Key t), FromJSON (Entry t))+            => JsonFileDatabase t -> IO (JsonFileDatabase t) loadInCache t@(JsonFileDatabase file _ status) = case status of   Empty -> do-    io $ createDirectoryIfMissing True $ takeDirectory file-    fileContent <- hGetContents =<< io (openFile file ReadWriteMode)+    createDirectoryIfMissing True $ takeDirectory file+    fileContent <- withBinaryFile file ReadWriteMode (toLazy_ . hGetContents)     cache <- (`failWith` UnableDecode) $ fmap Map.fromList $ decode $ fromEmpty "[]" fileContent     return $ JsonFileDatabase file cache Clean   _ -> return t@@ -88,41 +97,41 @@         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+insert :: (Table t, FromJSON (Key t), FromJSON (Entry t))+       => [(Key t, Entry t)] -> JsonFileDatabase t -> IO (JsonFileDatabase t)+insert rows t = 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 :: (Table t, FromJSON (Key t), FromJSON (Entry t))+       => JsonFileDatabase t -> Key t -> (Entry t -> Entry t) -> IO (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+delete :: (Table t, FromJSON (Key t), FromJSON (Entry t))+       => [Key t] -> JsonFileDatabase t -> IO (JsonFileDatabase t)+delete keys t = 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 :: (Table t, FromJSON (Key t), FromJSON (Entry t))+      => JsonFileDatabase t -> IO (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 :: (ToJSON (Key t), ToJSON (Entry t))+       => JsonFileDatabase t -> IO (JsonFileDatabase t) commit t@(JsonFileDatabase file cache status) = case status of   Dirty -> do-    writeFile file $ encode $ Map.toList cache+    withFile file WriteMode $ \h -> (hPut h $ encode $ Map.toList cache)     return $ JsonFileDatabase file cache Clean   _ -> return t
src/lib/Imm/Dyre.hs view
@@ -13,12 +13,13 @@  -- {{{ Imports import           Imm.Prelude+import           Imm.Pretty  import           Config.Dyre import           Config.Dyre.Compile import           Config.Dyre.Paths -import           Text.PrettyPrint.ANSI.Leijen hiding ((<$>))+import           System.IO -- }}}  -- | How dynamic reconfiguration process should behave.@@ -31,7 +32,7 @@   -- | Describe the paths used for dynamic reconfiguration-describePaths :: (MonadIO m) => m Doc+describePaths :: (MonadIO m) => m (Doc AnsiStyle) describePaths = io $ do   (a, b, c, d, e) <- getPaths baseParameters   return $ vsep@@ -43,7 +44,7 @@     ]  -- | Dynamic reconfiguration settings-parameters :: Mode -> (a -> IO ()) -> Params (Either Text a)+parameters :: Mode -> (a -> IO ()) -> Params (Either String a) parameters mode main = baseParameters     { configCheck = mode /= Vanilla     , realMain = main'@@ -52,10 +53,10 @@     main' (Left e)  = hPutStrLn stderr e     main' (Right x) = main x -baseParameters :: Params (Either Text a)+baseParameters :: Params (Either String a) baseParameters = defaultParams   { projectName             = "imm"-  , showError               = const (Left . fromString)+  , showError               = const Left   , ghcOpts                 = ["-threaded"]   , statusOut               = hPutStrLn stderr   , includeCurrentDirectory = False
src/lib/Imm/Error.hs view
@@ -1,5 +1,4 @@ {-# LANGUAGE NoImplicitPrelude   #-}-{-# LANGUAGE OverloadedStrings   #-} {-# LANGUAGE ScopedTypeVariables #-} module Imm.Error (module Imm.Error) where @@ -8,7 +7,7 @@ -- }}}  liftE :: (MonadThrow m, Exception e) => Either e a -> m a-liftE (Left e) = throwM e+liftE (Left e)  = throwM e liftE (Right a) = return a  -- | Wrap a 'Maybe' value in 'MonadError'
src/lib/Imm/Feed.hs view
@@ -1,6 +1,7 @@ {-# LANGUAGE DataKinds         #-} {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications  #-} -- | Helpers to manipulate feeds module Imm.Feed where @@ -26,7 +27,7 @@   deriving(Eq, Show)  instance Pretty FeedRef where-  pretty (ByUID n) = text "feed" <+> text (show n)+  pretty (ByUID n) = "feed" <+> pretty n   pretty (ByURI u) = prettyURI u  data Feed = Rss (RssDocument '[ContentModule, DublinCoreModule]) | Atom AtomFeed@@ -47,7 +48,7 @@ getElements (Atom feed) = map AtomElement $ feedEntries feed  getDate :: FeedElement -> Maybe UTCTime-getDate (RssElement item)   = itemPubDate item <|> (elementDate $ itemDcMetaData (item ^. itemExtensionL))+getDate (RssElement item)   = itemPubDate item <|> elementDate (itemDcMetaData $ item ^. itemExtensionL) getDate (AtomElement entry) = Just $ entryUpdated entry  getTitle :: FeedElement -> Text@@ -63,7 +64,7 @@   getHashes :: FeedElement -> [Int]-getHashes (RssElement item) = map (hash . (show :: Doc -> String) . prettyGuid) (maybeToList $ itemGuid item)+getHashes (RssElement item) = map (hash @String . show . prettyGuid) (maybeToList $ itemGuid item)   <> map ((hash :: String -> Int) . show . withRssURI prettyURI) (maybeToList $ itemLink item)   <> [hash $ itemTitle item]   <> [hash $ itemDescription item]@@ -72,6 +73,6 @@  -- * Misc -prettyElement :: FeedElement -> Doc+prettyElement :: FeedElement -> Doc a prettyElement (RssElement item)   = prettyItem item prettyElement (AtomElement entry) = prettyEntry entry
src/lib/Imm/HTTP.hs view
@@ -1,48 +1,29 @@-{-# LANGUAGE DeriveFunctor         #-}-{-# LANGUAGE FlexibleContexts      #-}-{-# LANGUAGE FlexibleInstances     #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE NoImplicitPrelude     #-}-{-# LANGUAGE OverloadedStrings     #-}-{-# LANGUAGE TypeOperators         #-}--- | DSL/interpreter model for the HTTP client+{-# LANGUAGE FlexibleContexts  #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}+-- | HTTP module abstracts over HTTP requests to the external world. module Imm.HTTP where  -- {{{ Imports-import           Imm.Error import           Imm.Logger import           Imm.Prelude import           Imm.Pretty -import           Control.Monad.Trans.Free- import           URI.ByteString -- }}}  -- * Types --- | HTTP client DSL-data HttpClientF next-  = Get URI (Either SomeException LByteString -> next)-  deriving(Functor)---- | HTTP client interpreter-newtype CoHttpClientF m a = CoHttpClientF-  { getH :: URI -> m (Either SomeException LByteString, a)-  } deriving(Functor)--instance Monad m => PairingM (CoHttpClientF m) HttpClientF m where-  -- pairM :: (a -> b -> m r) -> f a -> g b -> m r-  pairM p (CoHttpClientF g) (Get uri next) = do-    (result, a) <- g uri-    p a $ next result+-- | Monad capable of performing GET HTTP requests.+class MonadThrow m => MonadHttpClient m where+  httpGet :: URI -> m LByteString  -- * Primitives --- | Perform an HTTP GET request-get :: (MonadFree f m, HttpClientF :<: f, LoggerF :<: f, MonadThrow m)+-- | Simple wrapper around 'httpGet' that also logs the requested URI.+get :: (MonadHttpClient m, MonadLog m, MonadThrow m)     => URI -> m LByteString get uri = do   logDebug $ "Fetching " <> prettyURI uri-  result <- liftF . inj $ Get uri id-  liftE result+  httpGet uri
src/lib/Imm/HTTP/Simple.hs view
@@ -1,29 +1,28 @@+{-# LANGUAGE FlexibleContexts  #-}+{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-}--- | Simple HTTP client interpreter.--- For more information, please consult "Network.HTTP.Client".-module Imm.HTTP.Simple (defaultManager, mkCoHttpClient, module Reexport) where+-- | Implementation of "Imm.HTTP" based on "Network.HTTP.Client".+module Imm.HTTP.Simple (defaultManager, module Reexport) where  -- {{{ Imports import           Imm.HTTP import           Imm.Prelude import           Imm.Pretty +import           Control.Monad.Trans.Reader import           Data.CaseInsensitive--import           Network.Connection      as Reexport-import           Network.HTTP.Client     as Reexport-import           Network.HTTP.Client.TLS as Reexport-+import           Network.Connection         as Reexport+import           Network.HTTP.Client        as Reexport+import           Network.HTTP.Client.TLS    as Reexport import           URI.ByteString -- }}} --- | Interpreter for 'HttpClientF'-mkCoHttpClient :: (MonadIO m, MonadCatch m) => Manager -> CoHttpClientF m Manager-mkCoHttpClient manager = CoHttpClientF coGet where-  coGet uri = handleAny (\e -> return (Left e, manager)) $ do-    result <- httpGet manager uri-    return (Right result, manager)+-- | Monad capable of performing HTTP GET requests.+instance MonadHttpClient (ReaderT Manager IO) where+  httpGet uri = do+    manager <- ask+    lift $ httpGet' manager uri  -- | Default manager uses TLS and no proxy defaultManager :: IO Manager@@ -31,11 +30,10 @@   -- | Perform an HTTP GET request and return the response body-httpGet :: (MonadIO m, MonadThrow m)-    => Manager -> URI -> m LByteString-httpGet manager uri = do+httpGet' :: Manager -> URI -> IO LByteString+httpGet' manager uri = do   request <- makeRequest uri-  responseBody <$> io (httpLbs request manager)+  responseBody <$> httpLbs request manager     -- codec'   <- reader $ view (config.codec)     -- return $ response $=+ decode codec' @@ -43,7 +41,7 @@ parseRequest' = parseRequest . show . prettyURI  -- | Build an HTTP request for given URI-makeRequest :: (MonadIO m, MonadThrow m) => URI -> m Request+makeRequest :: URI -> IO Request makeRequest uri = do   req <- parseRequest' uri   return $ req { requestHeaders = [
src/lib/Imm/Hooks.hs view
@@ -1,11 +1,8 @@-{-# LANGUAGE DeriveFunctor         #-}-{-# LANGUAGE FlexibleContexts      #-}-{-# LANGUAGE FlexibleInstances     #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE NoImplicitPrelude     #-}-{-# LANGUAGE OverloadedStrings     #-}-{-# LANGUAGE TypeOperators         #-}--- | DSL/interpreter model for hooks, ie various events that can trigger arbitrary actions+{-# LANGUAGE FlexibleContexts  #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}+-- | Hooks module to define the main behavior of the program. module Imm.Hooks where  -- {{{ Imports@@ -13,31 +10,18 @@ import           Imm.Logger import           Imm.Prelude import           Imm.Pretty--import           Control.Monad.Free.Class -- }}}  -- * Types --- | Hooks DSL-data HooksF next-  = OnNewElement Feed FeedElement next-  deriving(Functor)---- | Hooks interpreter-data CoHooksF m a = CoHooksF-  { onNewElementH :: Feed -> FeedElement -> m a  -- ^ Triggered for each unread feed element-  } deriving(Functor)--instance Monad m => PairingM (CoHooksF m) HooksF m where-  -- pairM :: (a -> b -> m r) -> f a -> g b -> m r-  pairM p (CoHooksF f) (OnNewElement feed element next) = do-    a <- f feed element-    p a next+-- | Monad capable of acting on specific events.+class Monad m => MonadImm m where+  -- | Action triggered for each unread feed element+  processNewElement :: Feed -> FeedElement -> m ()  -- * Primitives -onNewElement :: (MonadFree f m, LoggerF :<: f, HooksF :<: f) => Feed -> FeedElement -> m ()+onNewElement :: (MonadImm m, MonadLog m) => Feed -> FeedElement -> m () onNewElement feed element = do-  logDebug $ "Unread element:" <+> textual (getTitle element)-  liftF . inj $ OnNewElement feed element ()+  logDebug $ "Unread element:" <+> pretty (getTitle element)+  processNewElement feed element
+ src/lib/Imm/Hooks/Dummy.hs view
@@ -0,0 +1,21 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}+-- | Implementation of "Imm.Hooks" that does nothing,+-- except suggesting the user to define proper hooks.+--+-- This is the default implementation of the program.+module Imm.Hooks.Dummy where++-- {{{ Imports+import           Imm.Hooks+import           Imm.Prelude++import           Control.Exception+import           Control.Monad.Trans.Reader+-- }}}++data DummyHooks = DummyHooks++instance MonadImm (ReaderT DummyHooks IO) where+  processNewElement _ _ = throwM $ NoMethodError "Please define a valid Imm.Hooks.processNewElement function"
src/lib/Imm/Hooks/SendMail.hs view
@@ -1,8 +1,9 @@+{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeOperators     #-}--- | Hooks interpreter that sends a mail via a SMTP server for each element.--- You may want to consult "Network.HaskellNet.SMTP", "Network.HaskellNet.SMTP.SSL" and "Network.Mail.Mime" modules for additional information.+-- | Implementation of "Imm.Hooks" that sends a mail via a SMTP server for each new RSS/Atom element.+-- You may want to check out "Network.HaskellNet.SMTP", "Network.HaskellNet.SMTP.SSL" and "Network.Mail.Mime" modules for additional information. -- -- Here is an example configuration: --@@ -29,19 +30,18 @@ import           Imm.Prelude import           Imm.Pretty +import           Control.Monad.Trans.Reader import           Data.NonNull import           Data.Time- import           Network.HaskellNet.SMTP     as Reexport import           Network.HaskellNet.SMTP.SSL as Reexport import           Network.Mail.Mime           as Reexport hiding (sendmail) import           Network.Socket- import           Text.Atom.Types import           Text.RSS.Types -- }}} --- * Settings+-- * Types  type Username = String type Password = String@@ -68,18 +68,16 @@  data SendMailSettings = SendMailSettings (Feed -> FeedElement -> SMTPServer) FormatMail +instance MonadImm (ReaderT SendMailSettings IO) where+  processNewElement feed element = do+    SendMailSettings connectionSettings formatMail <- ask+    timezone <- lift getCurrentTimeZone+    currentTime <- lift getCurrentTime+    let mail = buildMail formatMail currentTime timezone feed element+    lift $ withSMTPConnection (connectionSettings feed element) $ sendMimeMail2 mail --- * Interpreter --- | Interpreter for 'HooksF'-mkCoHooks :: (MonadIO m) => SendMailSettings -> CoHooksF m SendMailSettings-mkCoHooks a@(SendMailSettings connectionSettings formatMail) = CoHooksF coOnNewElement where-  coOnNewElement feed element = do-    timezone <- io getCurrentTimeZone-    currentTime <- io getCurrentTime-    let mail = buildMail formatMail currentTime timezone feed element-    io $ withSMTPConnection (connectionSettings feed element) $ sendMimeMail2 mail-    return a+-- * Default behavior  -- | Fill 'addressName' with the feed title and, if available, the authors' names. --@@ -93,7 +91,7 @@  -- | Fill mail subject with the element title defaultFormatSubject :: Feed -> FeedElement -> Text-defaultFormatSubject _ element = getTitle element+defaultFormatSubject _ = getTitle  -- | Fill mail body with: --
src/lib/Imm/Hooks/WriteFile.hs view
@@ -1,9 +1,8 @@ {-# LANGUAGE FlexibleContexts  #-}-{-# LANGUAGE NamedFieldPuns    #-}+{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TypeOperators     #-}--- | Hooks interpreter that writes a file for each element.+-- | Implementation of "Imm.Hooks" that writes a file for each new RSS/Atom item. module Imm.Hooks.WriteFile where  -- {{{ Imports@@ -13,39 +12,40 @@ import           Imm.Pretty  import           Control.Arrow+import           Control.Monad.Trans.Reader+import           Data.ByteString.Builder+import           Data.ByteString.Streaming     (toStreamingByteString) import           Data.Monoid.Textual           hiding (elem, map)-import qualified Data.Text.Lazy                as Text import           Data.Time+import           Streaming.With import           System.Directory              (createDirectoryIfMissing) import           System.FilePath import           Text.Atom.Types-import           Text.Blaze.Html.Renderer.Text+import           Text.Blaze.Html.Renderer.Utf8 import           Text.Blaze.Html5              (Html, docTypeHtml,                                                 preEscapedToHtml, (!))-import qualified Text.Blaze.Html5              as H hiding (map)+import qualified Text.Blaze.Html5              as H import           Text.Blaze.Html5.Attributes   as H (charset, href) import           Text.RSS.Types import           URI.ByteString -- }}} --- * Settings+-- * Types  -- | Where and what to write in a file-data FileInfo = FileInfo FilePath ByteString--data WriteFileSettings = WriteFileSettings (Feed -> FeedElement -> FileInfo)+data FileInfo = FileInfo FilePath Builder --- * Interpreter+newtype WriteFileSettings = WriteFileSettings (Feed -> FeedElement -> FileInfo) --- | Interpreter for 'HooksF'-mkCoHooks :: MonadIO m => WriteFileSettings -> CoHooksF m WriteFileSettings-mkCoHooks a@(WriteFileSettings f) = CoHooksF coOnNewElement where-  coOnNewElement feed element = do+instance MonadImm (ReaderT WriteFileSettings IO) where+  processNewElement feed element = do+    WriteFileSettings f <- ask     let FileInfo path content = f feed element-    io $ createDirectoryIfMissing True $ takeDirectory path-    writeFile path content-    return a+    lift $ createDirectoryIfMissing True $ takeDirectory path+    writeBinaryFile path $ toStreamingByteString content +-- * Default behavior+ -- | Wrapper around 'defaultFilePath' and 'defaultFileContent' defaultSettings :: FilePath            -- ^ Root directory for 'defaultFilePath'                 -> WriteFileSettings@@ -55,18 +55,18 @@  -- | Generate a path @<root>/<feed title>/<element date>-<element title>.html@, where @<root>@ is the first argument defaultFilePath :: FilePath -> Feed -> FeedElement -> FilePath-defaultFilePath root feed element = makeValid $ root </> feedTitle </> fileName <.> "html" where+defaultFilePath root feed element = makeValid $ root </> title </> fileName <.> "html" where   date = maybe "" (formatTime defaultTimeLocale "%F-") $ getDate element   fileName = date <> sanitize (convertText $ getTitle element)-  feedTitle = sanitize $ convertText $ getFeedTitle feed+  title = sanitize $ convertText $ getFeedTitle feed   sanitize = replaceIf isPathSeparator '-' >>> replaceAny ".?!#" '_'-  replaceAny :: [Char] -> Char -> String -> String+  replaceAny :: String -> Char -> String -> String   replaceAny list = replaceIf (`elem` list)   replaceIf f b = map (\c -> if f c then b else c)  -- | Generate an HTML page, with a title, a header and an article that contains the feed element-defaultFileContent :: Feed -> FeedElement -> ByteString-defaultFileContent feed element = encodeUtf8 $ Text.toStrict $ renderHtml $ docTypeHtml $ do+defaultFileContent :: Feed -> FeedElement -> Builder+defaultFileContent feed element = renderHtmlBuilder $ docTypeHtml $ do   H.head $ do     H.meta ! H.charset "utf-8"     H.title $ convertText $ getFeedTitle feed <> " | " <> getTitle element@@ -120,5 +120,5 @@ convertText :: (IsString t) => Text -> t convertText = fromString . toString (const "?") -convertDoc :: (IsString t) => Doc -> t+convertDoc :: (IsString t) => Doc a -> t convertDoc = show
src/lib/Imm/Logger.hs view
@@ -1,18 +1,14 @@-{-# LANGUAGE DeriveFunctor         #-} {-# LANGUAGE FlexibleContexts      #-} {-# LANGUAGE FlexibleInstances     #-} {-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE NamedFieldPuns        #-} {-# LANGUAGE NoImplicitPrelude     #-} {-# LANGUAGE OverloadedStrings     #-}-{-# LANGUAGE TypeOperators         #-}--- | DSL/interpreter model for the logger+-- | Logger module. module Imm.Logger where  -- {{{ Imports import           Imm.Prelude--import           Control.Monad.Trans.Free+import           Imm.Pretty -- }}}  -- * Types@@ -21,67 +17,22 @@   deriving(Eq, Ord, Read, Show)  instance Pretty LogLevel where-  pretty Debug   = text "DEBUG"-  pretty Info    = text "INFO"-  pretty Warning = text "WARNING"-  pretty Error   = text "ERROR"---- | Logger DSL-data LoggerF next-  = Log LogLevel Doc next-  | GetLevel (LogLevel -> next)-  | SetLevel LogLevel next-  | SetColorize Bool next-  | Flush next-  deriving(Functor)---- | Logger interpreter-data CoLoggerF m a = CoLoggerF-  { logH         :: LogLevel -> Doc -> m a-  , getLevelH    :: m (LogLevel, a)-  , setLevelH    :: LogLevel -> m a-  , setColorizeH :: Bool -> m a-  , flushH       :: m a-  } deriving(Functor)--instance Monad m => PairingM (CoLoggerF m) LoggerF m where-  -- pairM :: (a -> b -> m r) -> f a -> g b -> m r-  pairM p CoLoggerF{logH} (Log level message next) = do-    a <- logH level message-    p a next-  pairM p CoLoggerF{getLevelH} (GetLevel next) = do-    (l, a) <- getLevelH-    p a (next l)-  pairM p CoLoggerF{setLevelH} (SetLevel level next) = do-    a <- setLevelH level-    p a next-  pairM p CoLoggerF{setColorizeH} (SetColorize colorize next) = do-    a <- setColorizeH colorize-    p a next-  pairM p CoLoggerF{flushH} (Flush next) = do-    a <- flushH-    p a next---- * Primitives--log :: (MonadFree f m, LoggerF :<: f) => LogLevel -> Doc -> m ()-log level message = liftF . inj $ Log level message ()--getLogLevel :: (MonadFree f m, LoggerF :<: f) => m LogLevel-getLogLevel = liftF . inj $ GetLevel id--setLogLevel :: (MonadFree f m, LoggerF :<: f) => LogLevel -> m ()-setLogLevel level = liftF . inj $ SetLevel level ()--setColorizeLogs :: (MonadFree f m, LoggerF :<: f) => Bool -> m ()-setColorizeLogs colorize = liftF . inj $ SetColorize colorize ()+  pretty Debug   = "DEBUG"+  pretty Info    = "INFO"+  pretty Warning = "WARNING"+  pretty Error   = "ERROR" -flushLogs :: (MonadFree f m, LoggerF :<: f) => m ()-flushLogs = liftF . inj $ Flush ()+-- | Monad capable of logging pretty text.+class Monad m => MonadLog m where+  log :: LogLevel -> Doc AnsiStyle -> m ()+  getLogLevel :: m LogLevel+  setLogLevel :: LogLevel -> m ()+  setColorizeLogs :: Bool -> m ()+  flushLogs :: m ()  -- * Helpers -logDebug, logInfo, logWarning, logError :: (MonadFree f m, LoggerF :<: f) => Doc -> m ()+logDebug, logInfo, logWarning, logError :: MonadLog m => Doc AnsiStyle -> m () logDebug = log Debug logInfo = log Info logWarning = log Warning
src/lib/Imm/Logger/Simple.hs view
@@ -1,50 +1,60 @@+{-# LANGUAGE FlexibleContexts  #-}+{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--- | Simple logger interpreter.+-- | Implementation of "Imm.Logger" based on @fast-logger@. -- For further information, please consult "System.Log.FastLogger". module Imm.Logger.Simple (module Imm.Logger.Simple, module Reexport) where  -- {{{ Imports-import           Imm.Logger            as Reexport+import           Imm.Logger                                as Reexport import           Imm.Prelude import           Imm.Pretty -import           System.Log.FastLogger as Reexport+import           Control.Concurrent.MVar.Lifted+import           Control.Monad.Trans.Reader+import           Data.Text.Prettyprint.Doc.Render.Terminal+import           System.Log.FastLogger                     as Reexport -- }}} --- * Settings- data LoggerSettings = LoggerSettings-  { loggerSet      :: LoggerSet  -- ^ 'LoggerSet' used for 'Debug', 'Info' and 'Warning' logs-  , errorLoggerSet :: LoggerSet  -- ^ 'LoggerSet' used for 'Error' logs-  , logLevel       :: LogLevel   -- ^ Discard logs that are strictly less serious than this level-  , colorizeLogs   :: Bool       -- ^ Enable log colorisation+  { _loggerSet      :: LoggerSet  -- ^ 'LoggerSet' used for 'Debug', 'Info' and 'Warning' logs+  , _errorLoggerSet :: LoggerSet  -- ^ 'LoggerSet' used for 'Error' logs+  , _logLevel       :: LogLevel   -- ^ Discard logs that are strictly less serious than this level+  , _colorizeLogs   :: Bool       -- ^ Enable log colorisation   }  -- | Default logger forwards error messages to stderr, and other messages to stdout.-defaultLogger :: MonadIO m => m LoggerSettings-defaultLogger = io $ LoggerSettings+defaultLogger :: IO (MVar LoggerSettings)+defaultLogger = newMVar =<< LoggerSettings   <$> newStdoutLoggerSet defaultBufSize   <*> newStderrLoggerSet defaultBufSize   <*> pure Info   <*> pure True --- * Interpreter+instance MonadLog (ReaderT (MVar LoggerSettings) IO) where+  -- log :: LogLevel -> Doc -> m ()+  log l t = do+    settings <- readMVar =<< ask+    let loggerSet = (if l == Error then _errorLoggerSet else _loggerSet) settings+        handleColor = (\c -> if c then id else unAnnotate) $ _colorizeLogs settings+        refLevel = _logLevel settings+    when (l >= refLevel) $ lift $ pushLogStrLn loggerSet $ toLogStr $ renderLazy $ layoutPretty defaultLayoutOptions $ handleColor t --- | Interpreter for 'LoggerF'-mkCoLogger :: (MonadIO m) => LoggerSettings -> CoLoggerF m LoggerSettings-mkCoLogger settings = CoLoggerF coLog coGetLevel coSetLevel coSetColorize coFlush where-  coLog Error t = do-    io $ pushLogStrLn (errorLoggerSet settings) $ toLogStr $ (show :: Doc -> String) $ handleColor $ red t-    return settings-  coLog l t = do-    when (l >= logLevel settings) $ io $ pushLogStrLn (loggerSet settings) $ toLogStr $ (show :: Doc -> String) $ handleColor t-    return settings-  coGetLevel = return (logLevel settings, settings)-  coSetLevel l = return $ settings { logLevel = l }-  coSetColorize c = return $ settings { colorizeLogs = c }-  coFlush = do-    io $ flushLogStr $ loggerSet settings-    io $ flushLogStr $ errorLoggerSet settings-    return settings-  handleColor = if colorizeLogs settings then id else plain+  -- getLogLevel :: m LogLevel+  getLogLevel = _logLevel <$> (readMVar =<< ask)++  -- setLogLevel :: LogLevel -> m ()+  setLogLevel level = do+    mvar <- ask+    modifyMVar_ mvar $ \settings -> return (settings { _logLevel = level })++  -- setColorizeLogs :: Bool -> m ()+  setColorizeLogs value = do+    mvar <- ask+    modifyMVar_ mvar $ \settings -> return (settings { _colorizeLogs = value })++  -- flushLogs :: m ()+  flushLogs = do+    settings <- readMVar =<< ask+    lift $ flushLogStr $ _loggerSet settings+    lift $ flushLogStr $ _errorLoggerSet settings
src/lib/Imm/Options.hs view
@@ -5,17 +5,19 @@ module Imm.Options where  -- {{{ Imports-import           Imm.Dyre                    as Dyre (Mode (..))-import qualified Imm.Dyre                    as Dyre+import           Imm.Dyre                       as Dyre (Mode (..))+import qualified Imm.Dyre                       as Dyre import           Imm.Feed-import           Imm.Logger                  as Logger+import           Imm.Logger                     as Logger import           Imm.Prelude import           Imm.Pretty -import           Options.Applicative.Builder-import           Options.Applicative.Extra-import           Options.Applicative.Types-+import           Data.Set                       (Set)+import qualified Data.Set                       as Set+import qualified Data.Text                      as Text+import           Options.Applicative+import           Options.Applicative.Help.Core  as Help+import           Options.Applicative.Help.Types import           URI.ByteString -- }}} @@ -27,8 +29,9 @@              | Unread (Maybe FeedRef)              | Run (Maybe FeedRef)              | Show (Maybe FeedRef)+             | Help              | ShowVersion-             | Subscribe URI (Maybe Text)+             | Subscribe URI (Set Text)              | Unsubscribe (Maybe FeedRef)  deriving instance Eq Command@@ -42,6 +45,7 @@   pretty (Unread f)      = "Mark feed(s) as unread:" <+> pretty f   pretty (Run f)         = "Download new entries from feed(s):" <+> pretty f   pretty (Show f)        = "Show status for feed(s):" <+> pretty f+  pretty Help            = "Display help"   pretty ShowVersion     = "Show program version"   pretty (Subscribe f _) = "Subscribe to feed:" <+> prettyURI f   pretty (Unsubscribe f) = "Unsubscribe from feed(s):" <+> pretty f@@ -67,11 +71,16 @@ --         ] --         ++ catMaybes [("CONFIG=" ++) <$> opts^.configurationLabel_] +helpString :: Text+helpString = Text.pack $ renderHelp 100 $ Help.parserHelp defaultPrefs optionsParser+ parseOptions :: (MonadIO m) => m CliOptions-parseOptions = io $ customExecParser (defaultPrefs {- noBacktrack -} ) (info parser $ progDesc "Fetch elements from RSS/Atom feeds and execute arbitrary actions for each of them.")-  where parser = helper <*> optional dyreMasterBinary *> optional dyreDebug *> cliOptions+parseOptions = io $ customExecParser defaultPrefs (info optionsParser $ progDesc "Fetch elements from RSS/Atom feeds and execute arbitrary actions for each of them.")  +optionsParser :: Parser CliOptions+optionsParser = optional dyreMasterBinary *> optional dyreDebug *> cliOptions+ cliOptions :: Parser CliOptions cliOptions = CliOptions   <$> commands@@ -82,16 +91,19 @@  commands :: Parser Command commands = subparser $ mconcat-  [ command "check" . info (Check <$> optional feedRefOption) $ progDesc "Check availability and validity of all feed sources currently configured, without writing any mail."-  , command "import" . info (pure Import) $ progDesc "Import feeds list from an OPML descriptor (read from stdin)."-  , command "read" . info (Read <$> optional feedRefOption) $ progDesc "Mark given feed as read."-  , command "rebuild" . info (pure Rebuild) $ progDesc "Rebuild configuration file."-  , command "run" . info (Run <$> optional feedRefOption) $ progDesc "Update list of feeds."-  , command "show" . info (Show <$> optional feedRefOption) $ progDesc "List all feed sources currently configured, along with their status."-  , command "subscribe" . info subscribeOptions $ progDesc "Subscribe to a feed."-  , command "unread" . info (Unread <$> optional feedRefOption) $ progDesc "Mark given feed as unread."-  , command "unsubscribe" . info unsubscribeOptions $ progDesc "Unsubscribe from a feed."-  , command "version" . info (pure ShowVersion) $ progDesc "Print version."+  [ command "add" $ info subscribeOptions $ progDesc "Alias for subscribe."+  , command "check" $ info (Check <$> optional feedRefOption) $ progDesc "Check availability and validity of all feed sources currently configured, without writing any mail."+  , command "help" $ info (pure Help) $ progDesc "Display help"+  , command "import" $ info (pure Import) $ progDesc "Import feeds list from an OPML descriptor (read from stdin)."+  , command "read" $ info (Read <$> optional feedRefOption) $ progDesc "Mark given feed as read."+  , command "rebuild" $ info (pure Rebuild) $ progDesc "Rebuild configuration file."+  , command "remove" $ info unsubscribeOptions $ progDesc "Alias for unsubscribe."+  , command "run" $ info (Run <$> optional feedRefOption) $ progDesc "Update list of feeds."+  , command "show" $ info (Show <$> optional feedRefOption) $ progDesc "List all feed sources currently configured, along with their status."+  , command "subscribe" $ info subscribeOptions $ progDesc "Subscribe to a feed."+  , command "unread" $ info (Unread <$> optional feedRefOption) $ progDesc "Mark given feed as unread."+  , command "unsubscribe" $ info unsubscribeOptions $ progDesc "Unsubscribe from a feed."+  , command "version" $ info (pure ShowVersion) $ progDesc "Print version."   ]  @@ -119,11 +131,11 @@ -- }}}  -- {{{ Other options-configLabelOption :: Parser Text-configLabelOption = option auto $ long "config" <> short 'C' <> metavar "CONFIG" <> help "Use the given configuration for all operations."+tagOption :: Parser Text+tagOption = option auto $ long "tag" <> short 't' <> metavar "TAG" <> help "Set the given tag."  subscribeOptions, unsubscribeOptions :: Parser Command-subscribeOptions    = Subscribe <$> uriArgument "URI to subscribe to." <*> optional configLabelOption+subscribeOptions    = Subscribe <$> uriArgument "URI to subscribe to." <*> (Set.fromList <$> many tagOption) unsubscribeOptions  = Unsubscribe <$> optional feedRefOption -- }}} 
src/lib/Imm/Prelude.hs view
@@ -1,50 +1,31 @@-{-# LANGUAGE ConstraintKinds        #-}-{-# LANGUAGE EmptyDataDecls         #-}-{-# LANGUAGE FlexibleContexts       #-}-{-# LANGUAGE FlexibleInstances      #-}-{-# LANGUAGE FunctionalDependencies #-}-{-# LANGUAGE MultiParamTypeClasses  #-}-{-# LANGUAGE OverloadedStrings      #-}-{-# LANGUAGE RankNTypes             #-}-{-# LANGUAGE ScopedTypeVariables    #-}-{-# LANGUAGE TupleSections          #-}-{-# LANGUAGE TypeFamilies           #-}-{-# LANGUAGE TypeOperators          #-}-{-# LANGUAGE UndecidableInstances   #-}+{-# LANGUAGE FlexibleContexts #-} module Imm.Prelude (module Imm.Prelude, module X) where  -- {{{ Imports import           Control.Applicative             as X-import           Control.Comonad-import           Control.Comonad.Cofree import           Control.Exception.Safe          as X import           Control.Monad                   as X (MonadPlus (..), unless,                                                        void, when)+import           Control.Monad.Base              as X import           Control.Monad.IO.Class          as X-import           Control.Monad.Trans.Free        (FreeF (..), FreeT (..))+import           Control.Monad.Trans             as X (lift) import           Data.Bifunctor                  as X import qualified Data.ByteString                 as B (ByteString) import qualified Data.ByteString.Lazy            as LB (ByteString) import           Data.Containers                 as X import           Data.Either                     as X import           Data.Foldable                   as X (forM_)-import           Data.Functor.Identity-import           Data.Functor.Product-import           Data.Functor.Sum-import           Data.IOData                     as X-import           Data.Map                        as X (Map) import           Data.Maybe                      as X hiding (catMaybes)-import           Data.Monoid                     as X hiding (Product, Sum) import           Data.Monoid.Textual             as X (TextualMonoid, fromText) import           Data.MonoTraversable.Unprefixed as X hiding (forM_, mapM_) import           Data.Ord                        as X import           Data.Sequences                  as X import           Data.String                     as X (IsString (..))-import           Data.Tagged import qualified Data.Text                       as T (Text)+import           Data.Text.IO                    as X (getLine, putStr,+                                                       putStrLn) import qualified Data.Text.Lazy                  as LT (Text) import           Data.Traversable                as X (for, forM)-import           Data.Typeable                   as X import qualified GHC.Show                        as Show import           Prelude                         as X hiding (all, and, any,                                                        break, concat, concatMap,@@ -53,92 +34,17 @@                                                        getLine, length, lines,                                                        log, lookup, notElem,                                                        null, or, product,+                                                       putStr, putStrLn,                                                        readFile, replicate,                                                        reverse, sequence_, show,                                                        span, splitAt, sum, take,                                                        takeWhile, unlines,                                                        unwords, words,                                                        writeFile)-import           System.IO                       as X (stderr, stdout)-import           Text.PrettyPrint.ANSI.Leijen    as X (Doc, Pretty (..), angles,-                                                       brackets, equals, hsep,-                                                       indent, space, text,-                                                       vsep, (<+>))-import           Text.PrettyPrint.ANSI.Leijen    (line)+import           System.IO                       as X (IOMode (..), stderr,+                                                       stdout) -- }}} --- * Free monad utilities---- | Right-associative tuple type-constructor-type a ::: b = (a, b)-infixr 0 :::---- | Right-associative tuple data-constructor-(+:) :: a -> b -> (a,b)-(+:) a b = (a, b)-infixr 0 +:--(*:) :: (Functor f, Functor g) => (a -> f a) -> (b -> g b) -> (a, b) -> Product f g (a, b)-(*:) f g (a,b) = Pair ((,b) <$> f a) ((a,) <$> g b)-infixr 0 *:---data HLeft-data HRight-data HId-data HNo--type family Contains a b where-  Contains a a         = HId-  Contains a (Sum a b) = HLeft-  Contains a (Sum b c) = (HRight, Contains a c)-  Contains a b         = HNo--class Sub i sub sup where-  inj' :: Tagged i (sub a -> sup a)--instance Sub HId a a where-  inj' = Tagged id--instance Sub HLeft a (Sum a b) where-  inj' = Tagged InL--instance (Sub x f g) => Sub (HRight, x) f (Sum h g) where-  inj' = Tagged $ InR . proxy inj' (Proxy :: Proxy x)----- | A constraint @f :<: g@ expresses that @f@ is subsumed by @g@,--- i.e. @f@ can be used to construct elements in @g@.-class (Functor sub, Functor sup) => sub :<: sup where-  inj :: sub a -> sup a--instance (Functor f, Functor g, Sub (Contains f g) f g) => f :<: g where-  inj = proxy inj' (Proxy :: Proxy (Contains f g))----- | Functors @f@ and @g@ are paired when they can annihilate each other-class (Monad m, Functor f, Functor g) => PairingM f g m | f -> g where-  pairM :: (a -> b -> m r) -> f a -> g b -> m r--instance (Monad m) => PairingM Identity Identity m where-  pairM f (Identity a) (Identity b) = f a b--instance (PairingM f f' m, PairingM g g' m) => PairingM (Sum f g) (Product f' g') m where-  pairM p (InL x) (Pair a _) = pairM p x a-  pairM p (InR x) (Pair _ b) = pairM p x b--instance (PairingM f f' m, PairingM g g' m) => PairingM (Product f g) (Sum f' g') m where-  pairM p (Pair a _) (InL x) = pairM p a x-  pairM p (Pair _ b) (InR x) = pairM p b x--interpret :: (PairingM f g m) => (a -> b -> m r) -> Cofree f a -> FreeT g m b -> m r-interpret p eval program = do-  let a = extract eval-  b <- runFreeT program-  case b of-    Pure x  -> p a x-    Free gs -> pairM (interpret p) (unwrap eval) gs- -- * Shortcuts  type LByteString = LB.ByteString@@ -146,14 +52,12 @@ type LText = LT.Text type Text = T.Text --- | Generic 'Show.show'-show :: (Show a, IsString b) => a -> b-show = fromString . Show.show- -- | Shortcut to 'liftIO' io :: MonadIO m => IO a -> m a io = liftIO --- | Infix operator for 'line'-(<++>) :: Doc -> Doc -> Doc-x <++> y = x <> line <> y+-- * Generalisation++-- | Generic 'Show.show'+show :: (Show a, IsString b) => a -> b+show = fromString . Show.show
src/lib/Imm/Pretty.hs view
@@ -1,37 +1,38 @@-{-# LANGUAGE FlexibleContexts      #-}-{-# LANGUAGE FlexibleInstances     #-}-{-# LANGUAGE NoImplicitPrelude     #-}-{-# LANGUAGE OverloadedStrings     #-}-{-# LANGUAGE PartialTypeSignatures #-}+{-# LANGUAGE FlexibleContexts  #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-} module Imm.Pretty (module Imm.Pretty, module X) where  -- {{{ Imports import           Imm.Prelude -import           Data.Monoid.Textual import           Data.NonNull import           Data.Time import           Data.Tree -import           Text.Atom.Types              as Atom+import           Text.Atom.Types                           as Atom -- import           Text.OPML.Types              as OPML hiding (text) -- import qualified Text.OPML.Types              as OPML-import           Text.PrettyPrint.ANSI.Leijen as X hiding (sep, width, (<$>),-                                                    (</>), (<>))-import           Text.RSS.Types               as RSS-+import           Data.Text.Prettyprint.Doc                 as X hiding (list,+                                                                 width)+import           Data.Text.Prettyprint.Doc                 (list)+import           Data.Text.Prettyprint.Doc.Render.Terminal as X (AnsiStyle)+import           Data.Text.Prettyprint.Doc.Render.Terminal+import qualified Data.Text.Prettyprint.Doc.Render.Terminal as Pretty+import           Text.RSS.Types                            as RSS import           URI.ByteString -- }}} --- | Generalized 'text'-textual :: TextualMonoid t => t -> Doc-textual = text . toString (const "?")+-- | Infix operator for 'line'+(<++>) :: Doc a -> Doc a -> Doc a+x <++> y = x <> line <> y -prettyTree :: (Pretty a) => Tree a -> Doc+prettyTree :: (Pretty a) => Tree a -> Doc b prettyTree (Node n s) = pretty n <++> indent 2 (vsep $ prettyTree <$> s) -prettyTime :: UTCTime -> Doc-prettyTime = text . formatTime defaultTimeLocale rfc822DateFormat+prettyTime :: UTCTime -> Doc a+prettyTime = pretty . formatTime defaultTimeLocale rfc822DateFormat  -- instance Pretty OpmlHead where --   pretty h = hsep $ catMaybes@@ -59,20 +60,20 @@ -- instance Pretty Opml where --   pretty o = text "OPML" <+> pretty (opmlVersion o) <++> indent 2 (pretty (opmlHead o) <++> (vsep . map pretty $ opmlOutlines o)) -prettyPerson :: AtomPerson -> Doc-prettyPerson p = text (fromText $ toNullable $ personName p) <> email where+prettyPerson :: AtomPerson -> Doc a+prettyPerson p = pretty (toNullable $ personName p) <> email where   email = if null $ personEmail p     then mempty-    else space <> angles (text $ fromText $ personEmail p)+    else space <> angles (pretty $ personEmail p) -prettyLink :: AtomLink -> Doc+prettyLink :: AtomLink -> Doc a prettyLink l = withAtomURI prettyURI $ linkHref l -prettyAtomText :: AtomText -> Doc-prettyAtomText (AtomPlainText _ t) = text $ fromText t-prettyAtomText (AtomXHTMLText t)   = text $ fromText t+prettyAtomText :: AtomText -> Doc a+prettyAtomText (AtomPlainText _ t) = pretty t+prettyAtomText (AtomXHTMLText t)   = pretty t -prettyEntry :: AtomEntry -> Doc+prettyEntry :: AtomEntry -> Doc a prettyEntry e = "Entry:" <+> prettyAtomText (entryTitle e) <++> indent 4   (         "By" <+> equals <+> list (prettyPerson <$> entryAuthors e)   <++> "Updated" <+> equals <+> prettyTime (entryUpdated e)@@ -80,22 +81,40 @@   -- , "   Item Body:   " ++ (Imm.Mail.getItemContent item),   ) -prettyItem :: RssItem e -> Doc-prettyItem i = "Item:" <+> text (fromText $ itemTitle i) <++> indent 4-  (         "By" <+> equals <+> text (fromText $ itemAuthor i)-  <++> "Updated" <+> equals <+> fromMaybe "<empty>" (prettyTime <$> itemPubDate i)-  <++> "Link"    <+> equals <+> fromMaybe "<empty>" (withRssURI prettyURI <$> itemLink i)+prettyItem :: RssItem e -> Doc a+prettyItem i = "Item:" <+> pretty (itemTitle i) <++> indent 4+  (         "By" <+> equals <+> pretty (itemAuthor i)+  <++> "Updated" <+> equals <+> maybe "<empty>" prettyTime (itemPubDate i)+  <++> "Link"    <+> equals <+> maybe "<empty>" (withRssURI prettyURI) (itemLink i)   ) -prettyURI :: URIRef a -> Doc-prettyURI uri = text $ fromText $ decodeUtf8 $ serializeURIRef' uri+prettyURI :: URIRef a -> Doc b+prettyURI uri = pretty $ decodeUtf8 $ serializeURIRef' uri -prettyGuid :: RssGuid -> Doc-prettyGuid (GuidText t)         = text $ fromText t+prettyGuid :: RssGuid -> Doc a+prettyGuid (GuidText t)         = pretty t prettyGuid (GuidUri (RssURI u)) = prettyURI u -prettyAtomContent :: AtomContent -> Doc-prettyAtomContent (AtomContentInlineText _ t)  = text $ fromText t-prettyAtomContent (AtomContentInlineXHTML t)   = text $ fromText t-prettyAtomContent (AtomContentInlineOther _ t) = text $ fromText t+prettyAtomContent :: AtomContent -> Doc a+prettyAtomContent (AtomContentInlineText _ t)  = pretty t+prettyAtomContent (AtomContentInlineXHTML t)   = pretty t+prettyAtomContent (AtomContentInlineOther _ t) = pretty t prettyAtomContent (AtomContentOutOfLine _ u)   = withAtomURI prettyURI u++magenta :: Doc AnsiStyle -> Doc AnsiStyle+magenta = annotate $ color Magenta++yellow :: Doc AnsiStyle -> Doc AnsiStyle+yellow = annotate $ color Yellow++red :: Doc AnsiStyle -> Doc AnsiStyle+red = annotate $ color Red++green :: Doc AnsiStyle -> Doc AnsiStyle+green = annotate $ color Green++cyan :: Doc AnsiStyle -> Doc AnsiStyle+cyan = annotate $ color Cyan++bold :: Doc AnsiStyle -> Doc AnsiStyle+bold = annotate Pretty.bold
src/lib/Imm/XML.hs view
@@ -1,45 +1,14 @@-{-# LANGUAGE DeriveFunctor         #-}-{-# LANGUAGE FlexibleContexts      #-}-{-# LANGUAGE FlexibleInstances     #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE NoImplicitPrelude     #-}-{-# LANGUAGE TypeOperators         #-}--- | DSL/interpreter model for parsing XML into a 'Feed'+{-# LANGUAGE NoImplicitPrelude #-}+-- | XML module abstracts over the parsing of RSS/Atom feeds. module Imm.XML where  -- {{{ Imports-import           Imm.Error import           Imm.Feed import           Imm.Prelude -import           Control.Monad.Trans.Free- import           URI.ByteString -- }}} --- * Types---- | XML parsing DSL-data XmlParserF next-  = ParseXml URI LByteString (Either SomeException Feed -> next)-  deriving(Functor)---- | XML parsing interpreter-newtype CoXmlParserF m a = CoXmlParserF-  { parseXmlH :: URI -> LByteString -> m (Either SomeException Feed, a)-  } deriving(Functor)--instance Monad m => PairingM (CoXmlParserF m) XmlParserF m where-  -- pairM :: (a -> b -> m r) -> f a -> g b -> m r-  pairM f (CoXmlParserF p) (ParseXml uri bytestring next) = do-    (result, a) <- p uri bytestring-    f a $ next result---- * Primitives---- | Parse XML into a 'Feed'-parseXml :: (MonadFree f m, XmlParserF :<: f, MonadThrow m)-         => URI -> LByteString -> m Feed-parseXml uri bytestring = do-  result <- liftF . inj $ ParseXml uri bytestring id-  liftE result+-- | Monad capable of parsing XML into a 'Feed' (RSS or Atom).+class MonadThrow m => MonadXmlParser m where+  parseXml :: URI -> LByteString -> m Feed
+ src/lib/Imm/XML/Conduit.hs view
@@ -0,0 +1,36 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE RankNTypes        #-}+-- | Implementation of "Imm.XML" based on 'Conduit'.+module Imm.XML.Conduit where++-- {{{ Imports+import           Imm.Feed+import           Imm.Prelude+import           Imm.XML++import           Control.Monad+import           Control.Monad.Fix+import           Control.Monad.Trans.Reader+import           Data.Conduit+import           Data.XML.Types+import           Text.Atom.Conduit.Parse+import           Text.RSS.Conduit.Parse+import           Text.RSS1.Conduit.Parse+import           Text.XML.Stream.Parse+import           URI.ByteString+-- }}}++-- | A pre-process 'Conduit' can be set to alter the raw XML before feeding it to the parser,+-- depending on the feed 'URI'+newtype XmlParser = XmlParser (forall m . Monad m => URI -> ConduitT Event Event m ())++-- | 'Conduit' based implementation+instance (MonadIO m, MonadCatch m) => MonadXmlParser (ReaderT XmlParser m) where+  parseXml uri bytestring = do+    XmlParser preProcess <- ask+    lift $ runConduit $ parseLBS def bytestring .| preProcess uri .| force "Invalid feed" ((fmap Atom <$> atomFeed) `orE` (fmap Rss <$> rssDocument) `orE` (fmap Rss <$> rss1Document))++-- | Forward all 'Event's without any pre-process+defaultXmlParser :: XmlParser+defaultXmlParser = XmlParser $ const $ fix $ \loop -> await >>= maybe (return ()) (yield >=> const loop)
− src/lib/Imm/XML/Simple.hs
@@ -1,37 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--- | Simple interpreter to parse XML into 'Feed', based on 'Conduit'.-module Imm.XML.Simple where---- {{{ Imports-import           Imm.Feed-import           Imm.Prelude-import           Imm.XML--import           Control.Monad-import           Control.Monad.Fix--import           Data.Conduit-import           Data.XML.Types--import           Text.Atom.Conduit.Parse-import           Text.RSS.Conduit.Parse-import           Text.RSS1.Conduit.Parse-import           Text.XML.Stream.Parse--import           URI.ByteString--- }}}---- | A 'Conduit' to alter the raw XML before feeding it to the parser, depending on the feed 'URI'-type PreProcess m = URI -> Conduit Event m Event---- | Interpreter for 'XmlParserF'-mkCoXmlParser :: (MonadIO m, MonadCatch m) => PreProcess m -> CoXmlParserF m (PreProcess m)-mkCoXmlParser preProcess = CoXmlParserF coParse where-  coParse uri bytestring = handleAny (\e -> return (Left e, preProcess)) $ do-    result <- runConduit $ parseLBS def bytestring =$= preProcess uri =$= force "Invalid feed" ((fmap Atom <$> atomFeed) `orE` (fmap Rss <$> rssDocument) `orE` (fmap Rss <$> rss1Document))-    return (Right result, preProcess)---- | Default pre-process always forwards all 'Event's-defaultPreProcess :: Monad m => PreProcess m-defaultPreProcess _ = fix $ \loop -> await >>= maybe (return ()) (yield >=> const loop)