Hello community,
here is the log from the commit of package ghc-imm for openSUSE:Factory checked in at 2017-08-31 20:56:32
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/ghc-imm (Old)
and /work/SRC/openSUSE:Factory/.ghc-imm.new (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "ghc-imm"
Thu Aug 31 20:56:32 2017 rev:2 rq:513399 version:1.2.0.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/ghc-imm/ghc-imm.changes 2017-05-17 10:51:04.022572947 +0200
+++ /work/SRC/openSUSE:Factory/.ghc-imm.new/ghc-imm.changes 2017-08-31 20:56:32.714727365 +0200
@@ -1,0 +2,5 @@
+Thu Jul 27 14:07:10 UTC 2017 - psimons@suse.com
+
+- Update to version 1.2.0.0.
+
+-------------------------------------------------------------------
Old:
----
imm-1.1.0.0.tar.gz
New:
----
imm-1.2.0.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ ghc-imm.spec ++++++
--- /var/tmp/diff_new_pack.6Y5Bf8/_old 2017-08-31 20:56:33.570607111 +0200
+++ /var/tmp/diff_new_pack.6Y5Bf8/_new 2017-08-31 20:56:33.582605426 +0200
@@ -18,7 +18,7 @@
%global pkg_name imm
Name: ghc-%{pkg_name}
-Version: 1.1.0.0
+Version: 1.2.0.0
Release: 0
Summary: Execute arbitrary actions for each unread element of RSS/Atom feeds
License: WTFPL
@@ -71,6 +71,7 @@
BuildRequires: ghc-uri-bytestring-devel
BuildRequires: ghc-xml-conduit-devel
BuildRequires: ghc-xml-devel
+BuildRequires: ghc-xml-types-devel
BuildRoot: %{_tmppath}/%{name}-%{version}-build
%description
++++++ imm-1.1.0.0.tar.gz -> imm-1.2.0.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/imm-1.1.0.0/imm.cabal new/imm-1.2.0.0/imm.cabal
--- old/imm-1.1.0.0/imm.cabal 2016-10-24 22:15:11.000000000 +0200
+++ new/imm-1.2.0.0/imm.cabal 2017-03-19 10:26:16.000000000 +0100
@@ -1,9 +1,9 @@
name: imm
-version: 1.1.0.0
+version: 1.2.0.0
synopsis: Execute arbitrary actions for each unread element of RSS/Atom feeds
description: Cf README file
homepage: https://github.com/k0ral/imm
-license: OtherLicense
+license: PublicDomain
license-file: LICENSE
author: kamaradclimber, koral
maintainer: koral
@@ -33,6 +33,8 @@
Imm.Logger
Imm.Logger.Simple
Imm.Prelude
+ Imm.XML
+ Imm.XML.Simple
other-modules:
Imm.Aeson
Imm.Dyre
@@ -40,7 +42,7 @@
Imm.Options
Imm.Pretty
Paths_imm
- build-depends: aeson, 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, mono-traversable >= 1.0.0, network, opml-conduit >= 0.6, optparse-applicative, rainbow, rainbox, rss-conduit >= 0.3, safe-exceptions, tagged, text, transformers, time, timerep >= 2.0.0.0, tls, uri-bytestring, xml, xml-conduit, ansi-wl-pprint
+ 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, mono-traversable >= 1.0.0, network, opml-conduit >= 0.6, optparse-applicative, rainbow, rainbox, rss-conduit >= 0.3.1, safe-exceptions, tagged, text, transformers, 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
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/imm-1.1.0.0/src/bin/Executable.hs new/imm-1.2.0.0/src/bin/Executable.hs
--- old/imm-1.1.0.0/src/bin/Executable.hs 2016-08-23 12:03:21.000000000 +0200
+++ new/imm-1.2.0.0/src/bin/Executable.hs 2017-03-19 10:26:16.000000000 +0100
@@ -5,19 +5,19 @@
-- {{{ Imports
import Imm
import Imm.Database.JsonFile
-import qualified Imm.Hooks.WriteFile as WriteFile
import Imm.HTTP.Simple
import Imm.Logger.Simple
import Imm.Prelude
+import Imm.XML.Simple
import System.Exit
-- }}}
--- mkDummyCoHooks :: (MonadIO m, MonadThrow m) => () -> CoHooksF m ()
--- mkDummyCoHooks _ = CoHooksF coOnNewElement where
--- coOnNewElement _ _ = do
--- io $ putStrLn "No hook defined."
--- throwM $ ExitFailure 1
+mkDummyCoHooks :: (MonadIO m, MonadThrow m) => () -> CoHooksF m ()
+mkDummyCoHooks _ = CoHooksF coOnNewElement where
+ coOnNewElement _ _ = do
+ io $ putStrLn "No hook defined."
+ throwM $ ExitFailure 1
main :: IO ()
@@ -26,4 +26,4 @@
manager <- defaultManager
database <- defaultDatabase
- imm (mkCoHttpClient, manager) (mkCoDatabase, database) (mkCoLogger, logger) (WriteFile.mkCoHooks, WriteFile.defaultSettings "/home/koral/feeds")
+ imm (mkCoHttpClient, manager) (mkCoDatabase, database) (mkCoLogger, logger) (mkDummyCoHooks, ()) (mkCoXmlParser, defaultPreProcess)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/imm-1.1.0.0/src/lib/Imm/Boot.hs new/imm-1.2.0.0/src/lib/Imm/Boot.hs
--- old/imm-1.1.0.0/src/lib/Imm/Boot.hs 2016-10-24 22:08:58.000000000 +0200
+++ new/imm-1.2.0.0/src/lib/Imm/Boot.hs 2017-03-19 10:26:16.000000000 +0100
@@ -37,6 +37,7 @@
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
@@ -58,6 +59,7 @@
-- > import Imm.Hooks.SendMail
-- > import Imm.HTTP.Simple
-- > import Imm.Logger.Simple
+-- > import Imm.XML.Simple
-- >
-- > main :: IO ()
-- > main = do
@@ -65,7 +67,7 @@
-- > manager <- defaultManager
-- > database <- defaultDatabase
-- >
--- > imm (mkCoHttpClient, manager) (mkCoDatabase, database) (mkCoLogger, logger) (mkCoHooks, sendmail)
+-- > imm (mkCoHttpClient, manager) (mkCoDatabase, database) (mkCoLogger, logger) (mkCoHooks, sendmail) (mkCoXmlParser, defaultPreProcess)
-- >
-- > sendmail :: SendMailSettings
-- > sendmail = SendMailSettings smtpServer formatMail
@@ -82,14 +84,15 @@
-- > (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")
+ -> (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 = void $ do
+imm coHttpClient coDatabase coLogger coHooks coXmlParser = 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
+ where (next, start) = mkCoImm coHttpClient coDatabase coLogger coHooks coXmlParser
realMain :: (MonadIO m, PairingM (CoImmF m) ImmF m, MonadCatch m)
=> (Command, LogLevel, Bool, Cofree (CoImmF m) a) -> m ()
@@ -117,14 +120,22 @@
-- * DSL/interpreter model
-type CoImmF m = Product (CoHttpClientF m) (Product (CoDatabaseF' m) (Product (CoLoggerF m) (CoHooksF m)))
-type ImmF = Sum HttpClientF (Sum DatabaseF' (Sum LoggerF HooksF))
+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)
- -> ((a ::: b ::: c ::: d) -> CoImmF m (a ::: b ::: c ::: d), a ::: b ::: c ::: d)
-mkCoImm (coHttpClient, a) (coDatabase, b) (coLogger, c) (coHooks, d) =
- (coHttpClient *:* coDatabase *:* coLogger *:* coHooks, a >: b >: c >: d)
+ => (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
@@ -144,7 +155,7 @@
unless (null x || x == ("Y" :: Text)) $ throwM InterruptedException
-resolveTarget :: (MonadIO m, MonadThrow m, Functor f, MonadFree f m, DatabaseF' :<: f)
+resolveTarget :: (MonadIO m, MonadThrow m, MonadFree f m, DatabaseF' :<: f)
=> SafeGuard -> Maybe Core.FeedRef -> m [FeedID]
resolveTarget s Nothing = do
result <- keys <$> Database.fetchAll FeedTable
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/imm-1.1.0.0/src/lib/Imm/Core.hs new/imm-1.2.0.0/src/lib/Imm/Core.hs
--- old/imm-1.1.0.0/src/lib/Imm/Core.hs 2016-09-28 00:05:51.000000000 +0200
+++ new/imm-1.2.0.0/src/lib/Imm/Core.hs 2017-03-19 10:26:16.000000000 +0100
@@ -29,6 +29,7 @@
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
@@ -51,12 +52,8 @@
import System.Info
-import Text.Atom.Conduit.Parse
-import Text.Atom.Types
import Text.OPML.Conduit.Parse
import Text.OPML.Types as OPML
-import Text.RSS.Conduit.Parse
-import Text.RSS.Types
import Text.XML as XML ()
import Text.XML.Stream.Parse as XML
@@ -70,7 +67,7 @@
putStrLn $ "compiled by " ++ compilerName ++ "-" ++ showVersion compilerVersion
-- | Print database status for given feed(s)
-showFeed :: (MonadIO m, LoggerF :<: f, MonadThrow m, Functor f, MonadFree f m, DatabaseF' :<: f)
+showFeed :: (MonadIO m, LoggerF :<: f, MonadThrow m, MonadFree f m, DatabaseF' :<: f)
=> [FeedID] -> m ()
showFeed feedIDs = do
feeds <- Database.fetchList FeedTable feedIDs
@@ -78,38 +75,41 @@
if null feeds then logWarning "No subscription" else putBox $ entryTableToBox feeds
-- | Register the given feed URI in database
-subscribe :: (LoggerF :<: f, Functor f, MonadFree f m, DatabaseF' :<: f, MonadCatch m)
+subscribe :: (LoggerF :<: f, MonadFree f m, DatabaseF' :<: f, MonadCatch m)
=> URI -> Maybe Text -> m ()
subscribe uri category = Database.register (FeedID uri) $ fromMaybe "default" category
-- | Check for unread elements without processing them
-check :: (MonadIO m, MonadCatch m, LoggerF :<: f, Functor f, MonadFree f m, DatabaseF' :<: f, HttpClientF :<: f)
+check :: (MonadIO m, MonadCatch m, LoggerF :<: f, MonadFree f m, DatabaseF' :<: f, HttpClientF :<: f, XmlParserF :<: f)
=> [FeedID] -> m ()
check feedIDs = do
- results <- forM (zip ([1..] :: [Int]) feedIDs) $ \(i, feedID) -> 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
+ flushLogs
+
putBox $ statusTableToBox $ mapFromList $ zip feedIDs results
+
+ 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))
+
where width = length (show total :: String)
total = length feedIDs
-checkOne :: (MonadIO m, MonadCatch m, LoggerF :<: f, Functor f, MonadFree f m, DatabaseF' :<: f, HttpClientF :<: f)
+checkOne :: (MonadIO m, MonadCatch m, LoggerF :<: f, MonadFree f m, DatabaseF' :<: f, HttpClientF :<: f, XmlParserF :<: f)
=> FeedID -> m Int
-checkOne feedID@(FeedID uri) = do
- body <- HTTP.get uri
- feed <- runConduit $ parseLBS def body =$= force "Invalid feed" ((fmap Left <$> atomFeed) `orE` (fmap Right <$> rssDocument))
-
+checkOne feedID = do
+ feed <- getFeed feedID
case feed of
- Left _ -> logDebug $ "Parsed Atom feed: " <> pretty feedID
- Right _ -> logDebug $ "Parsed RSS feed: " <> pretty feedID
+ Atom _ -> logDebug $ "Parsed Atom feed: " <> pretty feedID
+ Rss _ -> logDebug $ "Parsed RSS feed: " <> pretty feedID
- let dates = either
- (map entryUpdated . feedEntries)
- (mapMaybe itemPubDate . channelItems)
- feed
+ let dates = mapMaybe getDate $ getElements feed
- logDebug $ vsep $ either (map prettyEntry . feedEntries) (map prettyItem . channelItems) feed
+ logDebug $ vsep $ map prettyElement $ getElements feed
status <- Database.getStatus feedID
return $ length $ filter (unread status) dates
@@ -117,10 +117,10 @@
unread _ _ = True
-run :: (MonadIO m, MonadCatch m, HooksF :<: f, LoggerF :<: f, Functor f, MonadFree f m, DatabaseF' :<: f, HttpClientF :<: f)
+run :: (MonadIO m, MonadCatch m, HooksF :<: f, LoggerF :<: f, MonadFree f m, DatabaseF' :<: f, HttpClientF :<: f, XmlParserF :<: f)
=> [FeedID] -> m ()
run feedIDs = do
- results <- forM (zip ([1..] :: [Int]) feedIDs) $ \(i, feedID) -> 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
return $ bimap (feedID,) (feedID,) result
@@ -136,14 +136,13 @@
where width = length (show total :: String)
total = length feedIDs
-runOne :: (MonadIO m, MonadCatch m, HooksF :<: f, LoggerF :<: f, Functor f, MonadFree f m, DatabaseF' :<: f, HttpClientF :<: f)
+runOne :: (MonadIO m, MonadCatch m, HooksF :<: f, LoggerF :<: f, MonadFree f m, DatabaseF' :<: f, HttpClientF :<: f, XmlParserF :<: f)
=> FeedID -> m ()
-runOne feedID@(FeedID uri) = do
- body <- HTTP.get uri
- feed <- runConduit $ parseLBS def body =$= force "Invalid feed" ((fmap Atom <$> atomFeed) `orE` (fmap Rss <$> rssDocument))
+runOne feedID = do
+ feed <- getFeed feedID
unreadElements <- filterM (fmap not . isRead feedID) $ getElements feed
- unless (null unreadElements) $ logInfo $ indent 2 $ green (pretty $ length unreadElements) <+> "unread element(s)"
+ unless (null unreadElements) $ logInfo $ indent 2 $ green (pretty $ length unreadElements) <+> "new element(s)"
forM_ unreadElements $ \element -> do
onNewElement feed element
@@ -152,7 +151,7 @@
Database.markAsRead feedID
-isRead :: (Functor f, MonadCatch m, DatabaseF' :<: f, MonadFree f m) => FeedID -> FeedElement -> m Bool
+isRead :: (MonadCatch m, DatabaseF' :<: f, MonadFree f 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
@@ -163,18 +162,23 @@
return $ matchHash || matchDate
-- | 'subscribe' to all feeds described by the OPML document provided in input (stdin)
-importOPML :: (MonadIO m, LoggerF :<: f, Functor f, MonadFree f m, DatabaseF' :<: f, MonadCatch m) => m ()
+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
forM_ (opmlOutlines opml) $ importOPML' mempty
-importOPML' :: (MonadIO m, LoggerF :<: f, Functor f, MonadFree f m, DatabaseF' :<: f, MonadCatch m)
+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' c (Node (OpmlOutlineSubscription _ s) _) = subscribe (xmlUri s) c
importOPML' _ _ = return ()
+getFeed :: (MonadIO m, MonadCatch m, MonadFree f m, HttpClientF :<: f, LoggerF :<: f, XmlParserF :<: f)
+ => FeedID -> m Feed
+getFeed (FeedID uri) = HTTP.get uri >>= parseXml uri
+
+
-- * Boxes
putBox :: (Orientation a, MonadIO m) => Box a -> m ()
@@ -202,6 +206,6 @@
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 (fromString . displayException) (const "OK") . 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]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/imm-1.1.0.0/src/lib/Imm/Database/FeedTable.hs new/imm-1.2.0.0/src/lib/Imm/Database/FeedTable.hs
--- old/imm-1.1.0.0/src/lib/Imm/Database/FeedTable.hs 2016-10-01 00:18:43.000000000 +0200
+++ new/imm-1.2.0.0/src/lib/Imm/Database/FeedTable.hs 2017-03-19 10:26:16.000000000 +0100
@@ -95,19 +95,19 @@
-- * Primitives
-register :: (MonadThrow m, LoggerF :<: f, DatabaseF' :<: f, Functor f, MonadFree f m)
+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
-getStatus :: (DatabaseF' :<: f, Functor f, MonadFree f m, MonadCatch m)
+getStatus :: (DatabaseF' :<: f, MonadFree f 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, Functor f, MonadFree f m, MonadThrow m, LoggerF :<: f)
+addReadHash :: (DatabaseF' :<: f, MonadFree f m, MonadThrow m, LoggerF :<: f)
=> FeedID -> Int -> m ()
addReadHash feedID hash = do
logDebug $ "Adding read hash: " <> pretty hash <> "..."
@@ -115,7 +115,7 @@
where f a = a { entryReadHashes = insertSet hash $ entryReadHashes a }
-- | Set the last check time to now
-markAsRead :: (MonadIO m, DatabaseF' :<: f, Functor f, MonadFree f m, MonadThrow m, LoggerF :<: f)
+markAsRead :: (MonadIO m, DatabaseF' :<: f, MonadFree f m, MonadThrow m, LoggerF :<: f)
=> FeedID -> m ()
markAsRead feedID = do
logDebug $ "Marking feed as read: " <> pretty feedID <> "..."
@@ -124,7 +124,7 @@
where f time a = a { entryLastCheck = Just time }
-- | Unset feed's last update and remove all read hashes
-markAsUnread :: (DatabaseF' :<: f, Functor f, MonadFree f m, MonadThrow m, LoggerF :<: f)
+markAsUnread :: (DatabaseF' :<: f, MonadFree f m, MonadThrow m, LoggerF :<: f)
=> FeedID -> m ()
markAsUnread feedID = do
logInfo $ "Marking feed as unread: " <> show (pretty feedID) <> "..."
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/imm-1.1.0.0/src/lib/Imm/Database/JsonFile.hs new/imm-1.2.0.0/src/lib/Imm/Database/JsonFile.hs
--- old/imm-1.1.0.0/src/lib/Imm/Database/JsonFile.hs 2016-08-22 15:15:42.000000000 +0200
+++ new/imm-1.2.0.0/src/lib/Imm/Database/JsonFile.hs 2017-03-19 10:26:16.000000000 +0100
@@ -39,9 +39,9 @@
mkJsonFileDatabase :: (Table t) => FilePath -> JsonFileDatabase t
mkJsonFileDatabase file = JsonFileDatabase file mempty Empty
--- | Default database is stored in @$XDG_DATA_HOME\/imm\/feeds.json@
+-- | Default database is stored in @$XDG_CONFIG_HOME\/imm\/feeds.json@
defaultDatabase :: Table t => IO (JsonFileDatabase t)
-defaultDatabase = mkJsonFileDatabase <$> getXdgDirectory XdgData "imm/feeds.json"
+defaultDatabase = mkJsonFileDatabase <$> getXdgDirectory XdgConfig "imm/feeds.json"
data JsonException = UnableDecode
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/imm-1.1.0.0/src/lib/Imm/Database.hs new/imm-1.2.0.0/src/lib/Imm/Database.hs
--- old/imm-1.1.0.0/src/lib/Imm/Database.hs 2016-09-27 23:01:07.000000000 +0200
+++ new/imm-1.2.0.0/src/lib/Imm/Database.hs 2017-03-19 10:26:16.000000000 +0100
@@ -111,62 +111,62 @@
-- * Primitives
-describeDatabase :: (Functor f, MonadFree f m, DatabaseF t :<: f)
+describeDatabase :: (MonadFree f m, DatabaseF t :<: f)
=> t -> m Doc
describeDatabase t = liftF . inj $ Describe t id
-fetch :: (Functor f, MonadFree f m, DatabaseF t :<: f, Table t, MonadThrow m)
+fetch :: (MonadFree f m, DatabaseF t :<: f, 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
-fetchList :: (Functor f, MonadFree f m, DatabaseF t :<: f, MonadThrow m)
+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
-fetchAll :: (MonadThrow m, Functor f, MonadFree f m, DatabaseF t :<: f) => t -> m (Map (Key t) (Entry t))
+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
-update :: (Functor f, MonadFree f m, DatabaseF t :<: f, MonadThrow m)
+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
-insert :: (MonadThrow m, Functor f, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f)
+insert :: (MonadThrow m, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f)
=> t -> Key t -> Entry t -> m ()
insert t k v = insertList t [(k, v)]
-insertList :: (MonadThrow m, Functor f, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f)
+insertList :: (MonadThrow m, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f)
=> 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
-delete :: (MonadThrow m, Functor f, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f) => t -> Key t -> m ()
+delete :: (MonadThrow m, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f) => t -> Key t -> m ()
delete t k = deleteList t [k]
-deleteList :: (MonadThrow m, Functor f, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f)
+deleteList :: (MonadThrow m, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f)
=> t -> [Key t] -> m ()
deleteList t k = do
logInfo $ "Deleting " <> yellow (pretty $ length k) <> " entries..."
result <- liftF . inj $ DeleteList t k id
liftE result
-purge :: (MonadThrow m, Functor f, MonadFree f m, DatabaseF t :<: f, LoggerF :<: f) => t -> m ()
+purge :: (MonadThrow m, MonadFree f m, DatabaseF t :<: f, LoggerF :<: f) => t -> m ()
purge t = do
logInfo "Purging database..."
result <- liftF . inj $ Purge t id
liftE result
-commit :: (MonadThrow m, Functor f, MonadFree f m, DatabaseF t :<: f, LoggerF :<: f) => t -> m ()
+commit :: (MonadThrow m, MonadFree f m, DatabaseF t :<: f, LoggerF :<: f) => t -> m ()
commit t = do
logDebug "Committing database transaction..."
result <- liftF . inj $ Commit t id
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/imm-1.1.0.0/src/lib/Imm/Feed.hs new/imm-1.2.0.0/src/lib/Imm/Feed.hs
--- old/imm-1.1.0.0/src/lib/Imm/Feed.hs 2016-10-09 14:19:07.000000000 +0200
+++ new/imm-1.2.0.0/src/lib/Imm/Feed.hs 2017-03-19 10:26:16.000000000 +0100
@@ -7,7 +7,6 @@
import Imm.Pretty
import Data.Hashable
-import Data.NonNull
import Data.Time
import Text.Atom.Types
@@ -57,3 +56,10 @@
<> [hash $ itemTitle item]
<> [hash $ itemDescription item]
getHashes (AtomElement entry) = [hash $ entryId entry, (hash :: String -> Int) $ show $ prettyAtomText $ entryTitle entry]
+
+
+-- * Misc
+
+prettyElement :: FeedElement -> Doc
+prettyElement (RssElement item) = prettyItem item
+prettyElement (AtomElement entry) = prettyEntry entry
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/imm-1.1.0.0/src/lib/Imm/HTTP.hs new/imm-1.2.0.0/src/lib/Imm/HTTP.hs
--- old/imm-1.1.0.0/src/lib/Imm/HTTP.hs 2016-09-27 23:51:58.000000000 +0200
+++ new/imm-1.2.0.0/src/lib/Imm/HTTP.hs 2017-03-19 10:26:16.000000000 +0100
@@ -40,7 +40,7 @@
-- * Primitives
-- | Perform an HTTP GET request
-get :: (MonadFree f m, Functor f, HttpClientF :<: f, LoggerF :<: f, MonadThrow m)
+get :: (MonadFree f m, HttpClientF :<: f, LoggerF :<: f, MonadThrow m)
=> URI -> m LByteString
get uri = do
logDebug $ "Fetching " <> prettyURI uri
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/imm-1.1.0.0/src/lib/Imm/Hooks/WriteFile.hs new/imm-1.2.0.0/src/lib/Imm/Hooks/WriteFile.hs
--- old/imm-1.1.0.0/src/lib/Imm/Hooks/WriteFile.hs 2016-10-16 22:24:40.000000000 +0200
+++ new/imm-1.2.0.0/src/lib/Imm/Hooks/WriteFile.hs 2017-03-19 10:26:16.000000000 +0100
@@ -1,3 +1,4 @@
+{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
@@ -11,7 +12,9 @@
import Imm.Prelude
import Imm.Pretty
-import Data.Monoid.Textual hiding (map)
+import Control.Arrow
+
+import Data.Monoid.Textual hiding (elem, map)
import qualified Data.Text.Lazy as Text
import Data.Time
@@ -19,9 +22,10 @@
import System.FilePath
import Text.Atom.Types
-import qualified Text.Blaze as Blaze
import Text.Blaze.Html.Renderer.Text
-import Text.Blaze.Html5 as H hiding (map)
+import Text.Blaze.Html5 (Html, docTypeHtml,
+ preEscapedToHtml, (!))
+import qualified Text.Blaze.Html5 as H hiding (map)
import Text.Blaze.Html5.Attributes as H (charset, href)
import Text.RSS.Types
@@ -57,9 +61,12 @@
defaultFilePath :: FilePath -> Feed -> FeedElement -> FilePath
defaultFilePath root feed element = makeValid $ root > feedTitle > fileName <.> "html" where
date = maybe "" (formatTime defaultTimeLocale "%F-") $ getDate element
- fileName = date <> convertText (sanitizePath $ getTitle element)
- feedTitle = convertText $ sanitizePath $ getFeedTitle feed
- sanitizePath = intercalate "-" . split isPathSeparator
+ fileName = date <> sanitize (convertText $ getTitle element)
+ feedTitle = sanitize $ convertText $ getFeedTitle feed
+ sanitize = replaceIf isPathSeparator '-' >>> replaceAny ".?!#" '_'
+ replaceAny :: [Char] -> 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
@@ -67,9 +74,9 @@
H.head $ do
H.meta ! H.charset "utf-8"
H.title $ convertText $ getFeedTitle feed <> " | " <> getTitle element
- body $ do
+ H.body $ do
H.h1 $ convertText $ getFeedTitle feed
- article $ do
+ H.article $ do
defaultHeader feed element
defaultBody feed element
@@ -78,29 +85,29 @@
-- | Generate an HTML @<header>@ for a given feed element
defaultHeader :: Feed -> FeedElement -> Html
-defaultHeader _ element@(RssElement item) = header $ do
+defaultHeader _ element@(RssElement item) = H.header $ do
H.h2 $ maybe id (\uri -> H.a ! H.href uri) link $ convertText $ getTitle element
- unless (null author) $ address $ "Published by " >> convertText author
- forM_ (itemPubDate item) $ \date -> p $ " on " >> time (convertDoc $ prettyTime date)
+ unless (null author) $ H.address $ "Published by " >> convertText author
+ forM_ (itemPubDate item) $ \date -> H.p $ " on " >> H.time (convertDoc $ prettyTime date)
where link = withRssURI (convertDoc . prettyURI) <$> itemLink item
author = itemAuthor item
-defaultHeader _ element@(AtomElement entry) = header $ do
+defaultHeader _ element@(AtomElement entry) = H.header $ do
H.h2 $ convertText $ getTitle element
- address $ do
+ H.address $ do
"Published by "
forM_ (entryAuthors entry) $ \author -> do
convertDoc $ prettyPerson author
", "
- p $ "on " >> time (convertDoc $ prettyTime $ entryUpdated entry)
+ H.p $ "on " >> H.time (convertDoc $ prettyTime $ entryUpdated entry)
-- | Generate the HTML content for a given feed element
defaultBody :: Feed -> FeedElement -> Html
-defaultBody _ (RssElement item) = p $ preEscapedToHtml $ itemDescription item
+defaultBody _ (RssElement item) = H.p $ preEscapedToHtml $ itemDescription item
defaultBody _ (AtomElement entry) = do
- unless (null links) $ p $ do
+ unless (null links) $ H.p $ do
"Related links:"
H.ul $ forM_ links $ \uri -> H.li (H.a ! H.href (convertAtomURI uri) $ convertAtomURI uri)
- p $ preEscapedToHtml $ fromMaybe "<empty>" $ content <|> summary
+ H.p $ preEscapedToHtml $ fromMaybe "<empty>" $ content <|> summary
where links = map linkHref $ entryLinks entry
content = show . prettyAtomContent <$> entryContent entry :: Maybe Text
summary = show . prettyAtomText <$> entrySummary entry :: Maybe Text
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/imm-1.1.0.0/src/lib/Imm/Hooks.hs new/imm-1.2.0.0/src/lib/Imm/Hooks.hs
--- old/imm-1.1.0.0/src/lib/Imm/Hooks.hs 2016-08-21 10:48:44.000000000 +0200
+++ new/imm-1.2.0.0/src/lib/Imm/Hooks.hs 2017-03-19 10:26:16.000000000 +0100
@@ -37,7 +37,7 @@
-- * Primitives
-onNewElement :: (Functor f, MonadFree f m, LoggerF :<: f, HooksF :<: f) => Feed -> FeedElement -> m ()
+onNewElement :: (MonadFree f m, LoggerF :<: f, HooksF :<: f) => Feed -> FeedElement -> m ()
onNewElement feed element = do
logDebug $ "Unread element:" <+> textual (getTitle element)
liftF . inj $ OnNewElement feed element ()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/imm-1.1.0.0/src/lib/Imm/Logger.hs new/imm-1.2.0.0/src/lib/Imm/Logger.hs
--- old/imm-1.1.0.0/src/lib/Imm/Logger.hs 2016-10-23 17:50:25.000000000 +0200
+++ new/imm-1.2.0.0/src/lib/Imm/Logger.hs 2017-03-19 10:26:16.000000000 +0100
@@ -64,24 +64,24 @@
-- * Primitives
-log :: (Functor f, MonadFree f m, LoggerF :<: f) => LogLevel -> Doc -> m ()
+log :: (MonadFree f m, LoggerF :<: f) => LogLevel -> Doc -> m ()
log level message = liftF . inj $ Log level message ()
-getLogLevel :: (Functor f, MonadFree f m, LoggerF :<: f) => m LogLevel
+getLogLevel :: (MonadFree f m, LoggerF :<: f) => m LogLevel
getLogLevel = liftF . inj $ GetLevel id
-setLogLevel :: (Functor f, MonadFree f m, LoggerF :<: f) => LogLevel -> m ()
+setLogLevel :: (MonadFree f m, LoggerF :<: f) => LogLevel -> m ()
setLogLevel level = liftF . inj $ SetLevel level ()
-setColorizeLogs :: (Functor f, MonadFree f m, LoggerF :<: f) => Bool -> m ()
+setColorizeLogs :: (MonadFree f m, LoggerF :<: f) => Bool -> m ()
setColorizeLogs colorize = liftF . inj $ SetColorize colorize ()
-flushLogs :: (Functor f, MonadFree f m, LoggerF :<: f) => m ()
+flushLogs :: (MonadFree f m, LoggerF :<: f) => m ()
flushLogs = liftF . inj $ Flush ()
-- * Helpers
-logDebug, logInfo, logWarning, logError :: (Functor f, MonadFree f m, LoggerF :<: f) => Doc -> m ()
+logDebug, logInfo, logWarning, logError :: (MonadFree f m, LoggerF :<: f) => Doc -> m ()
logDebug = log Debug
logInfo = log Info
logWarning = log Warning
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/imm-1.1.0.0/src/lib/Imm/Options.hs new/imm-1.2.0.0/src/lib/Imm/Options.hs
--- old/imm-1.1.0.0/src/lib/Imm/Options.hs 2016-10-12 20:21:56.000000000 +0200
+++ new/imm-1.2.0.0/src/lib/Imm/Options.hs 2017-03-19 10:26:16.000000000 +0100
@@ -68,7 +68,7 @@
-- ++ catMaybes [("CONFIG=" ++) <$> opts^.configurationLabel_]
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.")
+parseOptions = io $ customExecParser (prefs noBacktrack) (info parser $ progDesc "Convert items from RSS/Atom feeds to mails.")
where parser = helper <*> optional dyreMasterBinary *> optional dyreDebug *> cliOptions
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/imm-1.1.0.0/src/lib/Imm/Prelude.hs new/imm-1.2.0.0/src/lib/Imm/Prelude.hs
--- old/imm-1.1.0.0/src/lib/Imm/Prelude.hs 2016-10-23 19:10:49.000000000 +0200
+++ new/imm-1.2.0.0/src/lib/Imm/Prelude.hs 2017-03-19 10:26:16.000000000 +0100
@@ -45,7 +45,7 @@
import Data.Tagged
import qualified Data.Text as T (Text ())
import qualified Data.Text.Lazy as LT (Text ())
-import Data.Traversable as X (forM)
+import Data.Traversable as X (for, forM)
import Data.Typeable as X
import qualified GHC.Show as Show
@@ -81,13 +81,13 @@
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 *:*
+(+:) :: 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
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/imm-1.1.0.0/src/lib/Imm/Pretty.hs new/imm-1.2.0.0/src/lib/Imm/Pretty.hs
--- old/imm-1.1.0.0/src/lib/Imm/Pretty.hs 2016-08-23 01:04:31.000000000 +0200
+++ new/imm-1.2.0.0/src/lib/Imm/Pretty.hs 2017-03-19 10:26:16.000000000 +0100
@@ -16,7 +16,8 @@
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 ((<$>), (>), (<>))
+import Text.PrettyPrint.ANSI.Leijen as X hiding (sep, width, (<$>),
+ (>), (<>))
import Text.RSS.Types as RSS
import URI.ByteString
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/imm-1.1.0.0/src/lib/Imm/XML/Simple.hs new/imm-1.2.0.0/src/lib/Imm/XML/Simple.hs
--- old/imm-1.1.0.0/src/lib/Imm/XML/Simple.hs 1970-01-01 01:00:00.000000000 +0100
+++ new/imm-1.2.0.0/src/lib/Imm/XML/Simple.hs 2017-03-19 10:26:16.000000000 +0100
@@ -0,0 +1,37 @@
+{-# 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)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/imm-1.1.0.0/src/lib/Imm/XML.hs new/imm-1.2.0.0/src/lib/Imm/XML.hs
--- old/imm-1.1.0.0/src/lib/Imm/XML.hs 1970-01-01 01:00:00.000000000 +0100
+++ new/imm-1.2.0.0/src/lib/Imm/XML.hs 2017-03-19 10:26:16.000000000 +0100
@@ -0,0 +1,45 @@
+{-# LANGUAGE DeriveFunctor #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE TypeOperators #-}
+-- | DSL/interpreter model for parsing XML into a 'Feed'
+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