packages feed

rss-conduit 0.2.0.2 → 0.3.0.0

raw patch · 6 files changed

+201/−204 lines, 6 filesdep +conduit-combinatorsdep +safe-exceptionsdep −conduit-parsedep −exceptionsdep −foldldep ~conduitdep ~xml-conduitPVP ok

version bump matches the API change (PVP)

Dependencies added: conduit-combinators, safe-exceptions

Dependencies removed: conduit-parse, exceptions, foldl, parsers, xml-conduit-parse

Dependency ranges changed: conduit, xml-conduit

API changes (from Hackage documentation)

+ Text.RSS.Types: InvalidTime :: Text -> RssException
+ Text.RSS.Types: MissingElement :: Text -> RssException
- Text.RSS.Conduit.Parse: rssCategory :: MonadCatch m => ConduitParser Event m RssCategory
+ Text.RSS.Conduit.Parse: rssCategory :: MonadThrow m => ConduitM Event o m (Maybe RssCategory)
- Text.RSS.Conduit.Parse: rssCloud :: (MonadCatch m) => ConduitParser Event m RssCloud
+ Text.RSS.Conduit.Parse: rssCloud :: (MonadThrow m) => ConduitM Event o m (Maybe RssCloud)
- Text.RSS.Conduit.Parse: rssDocument :: MonadCatch m => ConduitParser Event m RssDocument
+ Text.RSS.Conduit.Parse: rssDocument :: MonadThrow m => ConduitM Event o m (Maybe RssDocument)
- Text.RSS.Conduit.Parse: rssEnclosure :: MonadCatch m => ConduitParser Event m RssEnclosure
+ Text.RSS.Conduit.Parse: rssEnclosure :: MonadThrow m => ConduitM Event o m (Maybe RssEnclosure)
- Text.RSS.Conduit.Parse: rssGuid :: MonadCatch m => ConduitParser Event m RssGuid
+ Text.RSS.Conduit.Parse: rssGuid :: MonadThrow m => ConduitM Event o m (Maybe RssGuid)
- Text.RSS.Conduit.Parse: rssImage :: (MonadCatch m) => ConduitParser Event m RssImage
+ Text.RSS.Conduit.Parse: rssImage :: (MonadThrow m) => ConduitM Event o m (Maybe RssImage)
- Text.RSS.Conduit.Parse: rssItem :: MonadCatch m => ConduitParser Event m RssItem
+ Text.RSS.Conduit.Parse: rssItem :: MonadThrow m => ConduitM Event o m (Maybe RssItem)
- Text.RSS.Conduit.Parse: rssSkipDays :: MonadCatch m => ConduitParser Event m (Set Day)
+ Text.RSS.Conduit.Parse: rssSkipDays :: MonadThrow m => ConduitM Event o m (Maybe (Set Day))
- Text.RSS.Conduit.Parse: rssSkipHours :: MonadCatch m => ConduitParser Event m (Set Hour)
+ Text.RSS.Conduit.Parse: rssSkipHours :: MonadThrow m => ConduitM Event o m (Maybe (Set Hour))
- Text.RSS.Conduit.Parse: rssSource :: MonadCatch m => ConduitParser Event m RssSource
+ Text.RSS.Conduit.Parse: rssSource :: MonadThrow m => ConduitM Event o m (Maybe RssSource)
- Text.RSS.Conduit.Parse: rssTextInput :: (MonadCatch m) => ConduitParser Event m RssTextInput
+ Text.RSS.Conduit.Parse: rssTextInput :: (MonadThrow m) => ConduitM Event o m (Maybe RssTextInput)

Files

README.md view
@@ -2,7 +2,7 @@  This [Haskell][hsk] library implements a streaming parser/renderer for the [RSS 2.0 syndication format][rss], based on [conduit][cdt]s. -Parsers are as much lenient as possible. E.g. unexpected tags are simply ignored.+Parsers are lenient as much as possible. E.g. unexpected tags are simply ignored.   [rss]: http://cyber.law.harvard.edu/rss/rss.html
Text/RSS/Conduit/Parse.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE OverloadedStrings  #-}+{-# LANGUAGE RankNTypes         #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell    #-} -- | Streaming parsers for the RSS 2.0 standard.@@ -21,19 +22,19 @@ -- {{{ Imports import           Text.RSS.Types -import           Control.Applicative-import           Control.Foldl                hiding (mconcat)+import           Conduit                      hiding (throwM)++import           Control.Applicative          hiding (many)+import           Control.Exception.Safe       as Exception import           Control.Monad                hiding (foldM)-import           Control.Monad.Catch+import           Control.Monad.Fix -import           Data.Conduit.Parser-import           Data.Conduit.Parser.XML+import           Data.Conduit import           Data.Maybe import           Data.Monoid import           Data.MonoTraversable import           Data.Set                     hiding (fold)-import           Data.Text                    as Text hiding (cons, last, map,-                                                       snoc)+import           Data.Text                    as Text import           Data.Text.Encoding import           Data.Time.Clock import           Data.Time.LocalTime@@ -45,9 +46,9 @@  import           Prelude                      hiding (last, lookup) -import           Text.Parser.Combinators import           Text.ParserCombinators.ReadP (readP_to_S) import           Text.Read                    (readMaybe)+import           Text.XML.Stream.Parse  import           URI.ByteString -- }}}@@ -57,175 +58,176 @@ asRssURI t = case (parseURI' t, parseRelativeRef' t) of   (Right u, _) -> return $ RssURI u   (_, Right u) -> return $ RssURI u-  (_, Left e) -> throwM $ InvalidURI e+  (_, Left e)  -> throwM $ InvalidURI e   where parseURI' = parseURI laxURIParserOptions . encodeUtf8         parseRelativeRef' = parseRelativeRef laxURIParserOptions . encodeUtf8 +nullURI :: RssURI+nullURI = RssURI $ RelativeRef Nothing "" (Query []) Nothing+ asInt :: MonadThrow m => Text -> m Int asInt t = maybe (throwM $ InvalidInt t) return . readMaybe $ unpack t  asBool :: MonadThrow m => Text -> m Bool-asBool "true" = return True+asBool "true"  = return True asBool "false" = return False-asBool t = throwM $ InvalidBool t+asBool t       = throwM $ InvalidBool t  asVersion :: MonadThrow m => Text -> m Version asVersion t = maybe (throwM $ InvalidVersion t) return . fmap fst . headMay . readP_to_S parseVersion $ unpack t  asCloudProtocol :: MonadThrow m => Text -> m CloudProtocol-asCloudProtocol "xml-rpc" = return ProtocolXmlRpc-asCloudProtocol "soap" = return ProtocolSoap+asCloudProtocol "xml-rpc"   = return ProtocolXmlRpc+asCloudProtocol "soap"      = return ProtocolSoap asCloudProtocol "http-post" = return ProtocolHttpPost-asCloudProtocol t = throwM $ InvalidProtocol t--unknownTag :: (MonadCatch m) => ConduitParser Event m (Endo a)-unknownTag = anyTag $ \_ _ -> many (void unknownTag <|> void textContent) >> return mempty+asCloudProtocol t           = throwM $ InvalidProtocol t  -- | Like 'tagName' but ignores the namespace.-tagName' :: (MonadCatch m) => Text -> AttrParser a -> (a -> ConduitParser Event m b) -> ConduitParser Event m b+tagName' :: (MonadThrow m) => Text -> AttrParser a -> (a -> ConduitM Event o m b) -> ConduitM Event o m (Maybe b) tagName' t = tagPredicate (\n -> nameLocalName n == t)  -- | Tag which content is a date-time that follows RFC 3339 format.-tagDate :: (MonadCatch m) => Name -> ConduitParser Event m UTCTime-tagDate name = tagIgnoreAttrs name $ content (fmap zonedTimeToUTC . parseTimeRFC822)+tagDate :: (MonadThrow m) => Name -> ConduitM Event o m (Maybe UTCTime)+tagDate name = tagIgnoreAttrs name $ fmap zonedTimeToUTC $ do+  text <- content+  maybe (throw $ InvalidTime text) return $ parseTimeRFC822 text +headRequiredC :: MonadThrow m => Text -> Consumer a m a+headRequiredC e = maybe (throw $ MissingElement e) return =<< headC -lastRequired :: (Monad m, Parsing m) => String -> FoldM m a a-lastRequired e = FoldM (\_ a -> return $ Right a) (return $ Left e) (either unexpected return)+projectC :: Monad m => Fold a a' b b' -> Conduit a m b+projectC prism = fix $ \recurse -> do+  item <- await+  case (item, item ^? (_Just . prism)) of+    (_, Just a) -> yield a >> recurse+    (Just _, _) -> recurse+    _           -> return () -- }}}   -- | Parse a @\<skipHours\>@ element.-rssSkipHours :: MonadCatch m => ConduitParser Event m (Set Hour)-rssSkipHours = named "Rss <skipHours> element" $ tagIgnoreAttrs "skipHours" $-  fromList <$> many (tagIgnoreAttrs "hour" $ content (asInt >=> asHour))+rssSkipHours :: MonadThrow m => ConduitM Event o m (Maybe (Set Hour))+rssSkipHours = tagIgnoreAttrs "skipHours" $+  fromList <$> (manyYield' (tagIgnoreAttrs "hour" $ content >>= asInt >>= asHour) =$= sinkList)  -- | Parse a @\<skipDays\>@ element.-rssSkipDays :: MonadCatch m => ConduitParser Event m (Set Day)-rssSkipDays = named "Rss <skipDays> element" $ tagIgnoreAttrs "skipDays" $-  fromList <$> many (tagIgnoreAttrs "day" $ content asDay)+rssSkipDays :: MonadThrow m => ConduitM Event o m (Maybe (Set Day))+rssSkipDays = tagIgnoreAttrs "skipDays" $+  fromList <$> (manyYield' (tagIgnoreAttrs "day" $ content >>= asDay) =$= sinkList)   data TextInputPiece = TextInputTitle Text | TextInputDescription Text                     | TextInputName Text | TextInputLink RssURI-                    | TextInputUnknown  makeTraversals ''TextInputPiece  -- | Parse a @\<textInput\>@ element.-rssTextInput :: (MonadCatch m) => ConduitParser Event m RssTextInput-rssTextInput = named "Rss <textInput> element" $ tagIgnoreAttrs "textInput" $ do-  p <- many piece-  flip foldM p $ RssTextInput-    <$> handlesM _TextInputTitle (lastRequired "Missing <title> element.")-    <*> handlesM _TextInputDescription (lastRequired "Missing <description> element.")-    <*> handlesM _TextInputName (lastRequired "Missing <name> element.")-    <*> handlesM _TextInputLink (lastRequired "Missing <link> element.")-  where piece :: MonadCatch m => ConduitParser Event m TextInputPiece-        piece = choice [ TextInputTitle <$> tagIgnoreAttrs "title" textContent-                       , TextInputDescription <$> tagIgnoreAttrs "description" textContent-                       , TextInputName <$> tagIgnoreAttrs "name" textContent-                       , TextInputLink <$> tagIgnoreAttrs "link" (content asRssURI)-                       , TextInputUnknown <$ unknownTag-                       ]+rssTextInput :: (MonadThrow m) => ConduitM Event o m (Maybe RssTextInput)+rssTextInput = tagIgnoreAttrs "textInput" $ (manyYield' (choose piece) =$= parser) <* many ignoreAllTreesContent where+  parser = getZipConduit $ RssTextInput+    <$> ZipConduit (projectC _TextInputTitle =$= headRequiredC "Missing <title> element")+    <*> ZipConduit (projectC _TextInputDescription =$= headRequiredC "Missing <description> element")+    <*> ZipConduit (projectC _TextInputName =$= headRequiredC "Missing <name> element")+    <*> ZipConduit (projectC _TextInputLink =$= headRequiredC "Missing <link> element")+  piece = [ fmap TextInputTitle <$> tagIgnoreAttrs "title" content+          , fmap TextInputDescription <$> tagIgnoreAttrs "description" content+          , fmap TextInputName <$> tagIgnoreAttrs "name" content+          , fmap TextInputLink <$> tagIgnoreAttrs "link" (content >>= asRssURI)+          ]  + data ImagePiece = ImageUri RssURI | ImageTitle Text | ImageLink RssURI | ImageWidth Int-                | ImageHeight Int | ImageDescription Text | ImageUnknown+                | ImageHeight Int | ImageDescription Text  makeTraversals ''ImagePiece  -- | Parse an @\<image\>@ element.-rssImage :: (MonadCatch m) => ConduitParser Event m RssImage-rssImage = named "Rss <image> element" $ tagIgnoreAttrs "image" $ do-  p <- many piece-  flip foldM p $ RssImage-    <$> handlesM _ImageUri (lastRequired "Missing <uri> element.")-    <*> handlesM _ImageTitle (lastRequired "Missing <title> element.")-    <*> handlesM _ImageLink (lastRequired "Missing <link> element.")-    <*> generalize (handles _ImageWidth last)-    <*> generalize (handles _ImageHeight last)-    <*> generalize (handles _ImageDescription $ lastDef "")-  where piece = choice [ ImageUri <$> tagIgnoreAttrs "uri" (content asRssURI)-                       , ImageTitle <$> tagIgnoreAttrs "title" textContent-                       , ImageLink <$> tagIgnoreAttrs "link" (content asRssURI)-                       , ImageWidth <$> tagIgnoreAttrs "width" (content asInt)-                       , ImageHeight <$> tagIgnoreAttrs "height" (content asInt)-                       , ImageDescription <$> tagIgnoreAttrs "description" textContent-                       , ImageUnknown <$ unknownTag-                       ]-+rssImage :: (MonadThrow m) => ConduitM Event o m (Maybe RssImage)+rssImage = tagIgnoreAttrs "image" $ (manyYield' (choose piece) =$= parser) <* many ignoreAllTreesContent where+  parser = getZipConduit $ RssImage+    <$> ZipConduit (projectC _ImageUri =$= headRequiredC "Missing <url> element")+    <*> ZipConduit (projectC _ImageTitle =$= headDefC "Unnamed image")  -- Lenient+    <*> ZipConduit (projectC _ImageLink =$= headDefC nullURI)  -- Lenient+    <*> ZipConduit (projectC _ImageWidth =$= headC)+    <*> ZipConduit (projectC _ImageHeight =$= headC)+    <*> ZipConduit (projectC _ImageDescription =$= headDefC "")+  piece = [ fmap ImageUri <$> tagIgnoreAttrs "url" (content >>= asRssURI)+          , fmap ImageTitle <$> tagIgnoreAttrs "title" content+          , fmap ImageLink <$> tagIgnoreAttrs "link" (content >>= asRssURI)+          , fmap ImageWidth <$> tagIgnoreAttrs "width" (content >>= asInt)+          , fmap ImageHeight <$> tagIgnoreAttrs "height" (content >>= asInt)+          , fmap ImageDescription <$> tagIgnoreAttrs "description" content+          ]  -- | Parse a @\<category\>@ element.-rssCategory :: MonadCatch m => ConduitParser Event m RssCategory-rssCategory = named "Rss <category> element" $ tagName' "category" (textAttr "domain") $ \domain ->-  RssCategory domain <$> textContent+rssCategory :: MonadThrow m => ConduitM Event o m (Maybe RssCategory)+rssCategory = tagName' "category" (optional (requireAttr "domain") <* ignoreAttrs) $ \domain ->+  RssCategory (fromMaybe "" domain) <$> content  -- | Parse a @\<cloud\>@ element.-rssCloud :: (MonadCatch m) => ConduitParser Event m RssCloud-rssCloud = named "Rss <cloud> element" $ tagName' "cloud" attributes return where+rssCloud :: (MonadThrow m) => ConduitM Event o m (Maybe RssCloud)+rssCloud = tagName' "cloud" attributes return where   attributes = do     uri <- fmap RssURI $ RelativeRef-      <$> fmap Just (Authority Nothing <$> (Host . encodeUtf8 <$> textAttr "domain") <*> (fmap Port <$> optional (attr "port" asInt)))-      <*> (encodeUtf8 <$> textAttr "path")+      <$> fmap Just (Authority Nothing <$> (Host . encodeUtf8 <$> requireAttr "domain") <*> (fmap Port <$> optional (requireAttr "port" >>= asInt)))+      <*> (encodeUtf8 <$> requireAttr "path")       <*> pure (Query [])       <*> pure Nothing-    RssCloud uri <$> textAttr "registerProcedure" <*> attr "protocol" asCloudProtocol <* ignoreAttrs+    RssCloud uri <$> requireAttr "registerProcedure" <*> (requireAttr "protocol" >>= asCloudProtocol) <* ignoreAttrs  -- | Parse a @\<guid\>@ element.-rssGuid :: MonadCatch m => ConduitParser Event m RssGuid-rssGuid = named "RSS <guid> element" $ tagName' "guid" attributes handler where-  attributes = optional (attr "isPermaLink" asBool) <* ignoreAttrs-  handler (Just True) = GuidUri <$> content asRssURI-  handler _ = GuidText <$> textContent+rssGuid :: MonadThrow m => ConduitM Event o m (Maybe RssGuid)+rssGuid = tagName' "guid" attributes handler where+  attributes = optional (requireAttr "isPermaLink" >>= asBool) <* ignoreAttrs+  handler (Just True) = GuidUri <$> (content >>= asRssURI)+  handler _           = GuidText <$> content  -- | Parse an @\<enclosure\>@ element.-rssEnclosure :: MonadCatch m => ConduitParser Event m RssEnclosure-rssEnclosure = named "Rss <enclosure> element" $ tagName' "enclosure" attributes handler where-  attributes = (,,) <$> attr "url" asRssURI <*> attr "length" asInt <*> textAttr "type" <* ignoreAttrs+rssEnclosure :: MonadThrow m => ConduitM Event o m (Maybe RssEnclosure)+rssEnclosure = tagName' "enclosure" attributes handler where+  attributes = (,,) <$> (requireAttr "url" >>= asRssURI) <*> (requireAttr "length" >>= asInt) <*> requireAttr "type" <* ignoreAttrs   handler (uri, length_, type_) = return $ RssEnclosure uri length_ type_  -- | Parse a @\<source\>@ element.-rssSource :: MonadCatch m => ConduitParser Event m RssSource-rssSource = named "Rss <source> element" $ tagName' "source" attributes handler where-  attributes = attr "url" asRssURI <* ignoreAttrs-  handler uri = RssSource uri <$> textContent+rssSource :: MonadThrow m => ConduitM Event o m (Maybe RssSource)+rssSource = tagName' "source" attributes handler where+  attributes = (requireAttr "url" >>= asRssURI) <* ignoreAttrs+  handler uri = RssSource uri <$> content   data ItemPiece = ItemTitle Text | ItemLink RssURI | ItemDescription Text                | ItemAuthor Text | ItemCategory RssCategory | ItemComments RssURI                | ItemEnclosure RssEnclosure | ItemGuid RssGuid | ItemPubDate UTCTime-               | ItemSource RssSource | ItemUnknown+               | ItemSource RssSource  makeTraversals ''ItemPiece  -- | Parse an @\<item\>@ element.-rssItem :: MonadCatch m => ConduitParser Event m RssItem-rssItem = named "Rss <item> element" $ tagIgnoreAttrs "item" $ do-  p <- many piece-  return . flip fold p $ RssItem-    <$> handles _ItemTitle (lastDef "")-    <*> handles _ItemLink last-    <*> handles _ItemDescription (lastDef "")-    <*> handles _ItemAuthor (lastDef "")-    <*> handles _ItemCategory list-    <*> handles _ItemComments last-    <*> handles _ItemEnclosure list-    <*> handles _ItemGuid last-    <*> handles _ItemPubDate last-    <*> handles _ItemSource last-  where piece = choice [ ItemTitle <$> tagIgnoreAttrs "title" textContent-                       , ItemLink <$> tagIgnoreAttrs "link" (content asRssURI)-                       , ItemDescription <$> tagIgnoreAttrs "description" textContent-                       , ItemAuthor <$> tagIgnoreAttrs "author" textContent-                       , ItemCategory <$> rssCategory-                       , ItemComments <$> tagIgnoreAttrs "comments" (content asRssURI)-                       , ItemEnclosure <$> rssEnclosure-                       , ItemGuid <$> rssGuid-                       , ItemPubDate <$> tagDate "pubDate"-                       , ItemSource <$> rssSource-                       , ItemUnknown <$ unknownTag-                       ]+rssItem :: MonadThrow m => ConduitM Event o m (Maybe RssItem)+rssItem = tagIgnoreAttrs "item" $ (manyYield' (choose piece) =$= parser) <* many ignoreAllTreesContent where+  parser = getZipConduit $ RssItem+    <$> ZipConduit (projectC _ItemTitle =$= headDefC "")+    <*> ZipConduit (projectC _ItemLink =$= headC)+    <*> ZipConduit (projectC _ItemDescription =$= headDefC "")+    <*> ZipConduit (projectC _ItemAuthor =$= headDefC "")+    <*> ZipConduit (projectC _ItemCategory =$= sinkList)+    <*> ZipConduit (projectC _ItemComments =$= headC)+    <*> ZipConduit (projectC _ItemEnclosure =$= sinkList)+    <*> ZipConduit (projectC _ItemGuid =$= headC)+    <*> ZipConduit (projectC _ItemPubDate =$= headC)+    <*> ZipConduit (projectC _ItemSource =$= headC)+  piece = [ fmap ItemTitle <$> tagIgnoreAttrs "title" content+          , fmap ItemLink <$> tagIgnoreAttrs "link" (content >>= asRssURI)+          , fmap ItemDescription <$> tagIgnoreAttrs "description" content+          , fmap ItemAuthor <$> tagIgnoreAttrs "author" content+          , fmap ItemCategory <$> rssCategory+          , fmap ItemComments <$> tagIgnoreAttrs "comments" (content >>= asRssURI)+          , fmap ItemEnclosure <$> rssEnclosure+          , fmap ItemGuid <$> rssGuid+          , fmap ItemPubDate <$> tagDate "pubDate"+          , fmap ItemSource <$> rssSource+          ]   data ChannelPiece = ChannelTitle Text | ChannelLink RssURI | ChannelDescription Text@@ -235,56 +237,53 @@                   | ChannelGenerator Text | ChannelDocs RssURI | ChannelCloud RssCloud                   | ChannelTtl Int | ChannelImage RssImage | ChannelRating Text                   | ChannelTextInput RssTextInput | ChannelSkipHours (Set Hour)-                  | ChannelSkipDays (Set Day) | ChannelUnknown+                  | ChannelSkipDays (Set Day)  makeTraversals ''ChannelPiece  -- | Parse an @\<rss\>@ element.-rssDocument :: MonadCatch m => ConduitParser Event m RssDocument-rssDocument = named "RSS <rss> element" $ tagName' "rss" attributes $ \version -> tagIgnoreAttrs "channel" $ do-  p <- many piece-  flip foldM p $ RssDocument version-    <$> handlesM _ChannelTitle (lastRequired "Missing <title> element.")-    <*> handlesM _ChannelLink (lastRequired "Missing <link> element.")-    <*> generalize (handles _ChannelDescription $ lastDef "")  -- Lenient-    <*> generalize (handles _ChannelItem list)-    <*> generalize (handles _ChannelLanguage $ lastDef "")-    <*> generalize (handles _ChannelCopyright $ lastDef "")-    <*> generalize (handles _ChannelManagingEditor $ lastDef "")-    <*> generalize (handles _ChannelWebmaster $ lastDef "")-    <*> generalize (handles _ChannelPubDate last)-    <*> generalize (handles _ChannelLastBuildDate last)-    <*> generalize (handles _ChannelCategory list)-    <*> generalize (handles _ChannelGenerator $ lastDef "")-    <*> generalize (handles _ChannelDocs last)-    <*> generalize (handles _ChannelCloud last)-    <*> generalize (handles _ChannelTtl last)-    <*> generalize (handles _ChannelImage last)-    <*> generalize (handles _ChannelRating $ lastDef "")-    <*> generalize (handles _ChannelTextInput last)-    <*> generalize (handles _ChannelSkipHours $ lastDef mempty)-    <*> generalize (handles _ChannelSkipDays $ lastDef mempty)-  where piece :: MonadCatch m => ConduitParser Event m ChannelPiece-        piece = choice [ ChannelTitle <$> tagIgnoreAttrs "title" textContent-                       , ChannelLink <$> tagIgnoreAttrs "link" (content asRssURI)-                       , ChannelDescription <$> tagIgnoreAttrs "description" textContent-                       , ChannelItem <$> rssItem-                       , ChannelLanguage <$> tagIgnoreAttrs "language" textContent-                       , ChannelCopyright <$> tagIgnoreAttrs "copyright" textContent-                       , ChannelManagingEditor <$> tagIgnoreAttrs "managingEditor" textContent-                       , ChannelWebmaster <$> tagIgnoreAttrs "webMaster" textContent-                       , ChannelPubDate <$> tagDate "pubDate"-                       , ChannelLastBuildDate <$> tagDate "lastBuildDate"-                       , ChannelCategory <$> rssCategory-                       , ChannelGenerator <$> tagIgnoreAttrs "generator" textContent-                       , ChannelDocs <$> tagIgnoreAttrs "docs" (content asRssURI)-                       , ChannelCloud <$> rssCloud-                       , ChannelTtl <$> tagIgnoreAttrs "ttl" (content asInt)-                       , ChannelImage <$> rssImage-                       , ChannelRating <$> tagIgnoreAttrs "rating" textContent-                       , ChannelTextInput <$> rssTextInput-                       , ChannelSkipHours <$> rssSkipHours-                       , ChannelSkipDays <$> rssSkipDays-                       , ChannelUnknown <$ unknownTag-                       ]-        attributes = attr "version" asVersion <* ignoreAttrs+rssDocument :: MonadThrow m => ConduitM Event o m (Maybe RssDocument)+rssDocument = tagName' "rss" attributes $ \version -> force "Missing <channel>" $ tagIgnoreAttrs "channel" (manyYield' (choose piece) =$= parser version) <* many ignoreAllTreesContent where+  parser version = getZipConduit $ RssDocument version+    <$> ZipConduit (projectC _ChannelTitle =$= headRequiredC "Missing <title> element")+    <*> ZipConduit (projectC _ChannelLink =$= headRequiredC "Missing <link> element")+    <*> ZipConduit (projectC _ChannelDescription =$= headDefC "")  -- Lenient+    <*> ZipConduit (projectC _ChannelItem =$= sinkList)+    <*> ZipConduit (projectC _ChannelLanguage =$= headDefC "")+    <*> ZipConduit (projectC _ChannelCopyright =$= headDefC "")+    <*> ZipConduit (projectC _ChannelManagingEditor =$= headDefC "")+    <*> ZipConduit (projectC _ChannelWebmaster =$= headDefC "")+    <*> ZipConduit (projectC _ChannelPubDate =$= headC)+    <*> ZipConduit (projectC _ChannelLastBuildDate =$= headC)+    <*> ZipConduit (projectC _ChannelCategory =$= sinkList)+    <*> ZipConduit (projectC _ChannelGenerator =$= headDefC "")+    <*> ZipConduit (projectC _ChannelDocs =$= headC)+    <*> ZipConduit (projectC _ChannelCloud =$= headC)+    <*> ZipConduit (projectC _ChannelTtl =$= headC)+    <*> ZipConduit (projectC _ChannelImage =$= headC)+    <*> ZipConduit (projectC _ChannelRating =$= headDefC "")+    <*> ZipConduit (projectC _ChannelTextInput =$= headC)+    <*> ZipConduit (projectC _ChannelSkipHours =$= headDefC mempty)+    <*> ZipConduit (projectC _ChannelSkipDays =$= headDefC mempty)+  piece = [ fmap ChannelTitle <$> tagIgnoreAttrs "title" content+          , fmap ChannelLink <$> tagIgnoreAttrs "link" (content >>= asRssURI)+          , fmap ChannelDescription <$> tagIgnoreAttrs "description" content+          , fmap ChannelItem <$> rssItem+          , fmap ChannelLanguage <$> tagIgnoreAttrs "language" content+          , fmap ChannelCopyright <$> tagIgnoreAttrs "copyright" content+          , fmap ChannelManagingEditor <$> tagIgnoreAttrs "managingEditor" content+          , fmap ChannelWebmaster <$> tagIgnoreAttrs "webMaster" content+          , fmap ChannelPubDate <$> tagDate "pubDate"+          , fmap ChannelLastBuildDate <$> tagDate "lastBuildDate"+          , fmap ChannelCategory <$> rssCategory+          , fmap ChannelGenerator <$> tagIgnoreAttrs "generator" content+          , fmap ChannelDocs <$> tagIgnoreAttrs "docs" (content >>= asRssURI)+          , fmap ChannelCloud <$> rssCloud+          , fmap ChannelTtl <$> tagIgnoreAttrs "ttl" (content >>= asInt)+          , fmap ChannelImage <$> rssImage+          , fmap ChannelRating <$> tagIgnoreAttrs "rating" content+          , fmap ChannelTextInput <$> rssTextInput+          , fmap ChannelSkipHours <$> rssSkipHours+          , fmap ChannelSkipDays <$> rssSkipDays+          ]+  attributes = (requireAttr "version" >>= asVersion) <* ignoreAttrs
Text/RSS/Conduit/Render.hs view
@@ -111,7 +111,7 @@   renderHost (Host h) = decodeUtf8 h   renderQuery (Query query) = case intercalate "&" $ map (\(a,b) -> decodeUtf8 a <> "=" <> decodeUtf8 b) query of     "" -> ""-    x -> "?" <> x+    x  -> "?" <> x    domain = maybe "" (\a -> renderUserInfo (authorityUserInfo a) <> renderHost (authorityHost a)) $ withRssURI (view authorityL) $ c^.cloudUriL   port = fmap (pack . show . portNumber) $ authorityPort =<< withRssURI (view authorityL) (c^.cloudUriL)@@ -119,8 +119,8 @@   query = renderQuery $ withRssURI (view queryL) $ c^.cloudUriL   fragment = maybe "" decodeUtf8 $ withRssURI (view fragmentL) $ c^.cloudUriL -  describe ProtocolXmlRpc = "xml-rpc"-  describe ProtocolSoap = "soap"+  describe ProtocolXmlRpc   = "xml-rpc"+  describe ProtocolSoap     = "soap"   describe ProtocolHttpPost = "http-post"  -- | Render a @\<category\>@ element.@@ -130,7 +130,7 @@ -- | Render an @\<image\>@ element. renderRssImage :: (Monad m) => RssImage -> Source m Event renderRssImage i = tag "image" mempty $ do-  textTag "uri" $ decodeUtf8 $ withRssURI serializeURIRef' $ i^.imageUriL+  textTag "url" $ decodeUtf8 $ withRssURI serializeURIRef' $ i^.imageUriL   textTag "title" $ i^.imageTitleL   textTag "link" $ decodeUtf8 $ withRssURI serializeURIRef' $ i^.imageLinkL   forM_ (i^.imageHeightL) $ textTag "height" . tshow
Text/RSS/Types.hs view
@@ -35,15 +35,15 @@ module Text.RSS.Types where  -- {{{ Imports-import           Control.Monad.Catch+import           Control.Exception.Safe  import           Data.Set-import           Data.Text           hiding (map)+import           Data.Text              hiding (map) import           Data.Time.Clock-import           Data.Time.LocalTime ()+import           Data.Time.LocalTime    () import           Data.Version -import           GHC.Generics        hiding ((:+:))+import           GHC.Generics           hiding ((:+:))  import           Text.Read @@ -58,6 +58,8 @@                   | InvalidURI URIParseError                   | InvalidVersion Text                   | InvalidProtocol Text+                  | InvalidTime Text+                  | MissingElement Text  deriving instance Eq RssException deriving instance Show RssException@@ -70,6 +72,8 @@   displayException (InvalidURI t) = "Invalid URI reference: " ++ show t   displayException (InvalidVersion t) = "Invalid version: " ++ unpack t   displayException (InvalidProtocol t) = "Invalid Protocol: expected \"xml-rpc\", \"soap\" or \"http-post\", got \"" ++ unpack t ++ "\""+  displayException (InvalidTime t) = "Invalid time: " ++ unpack t+  displayException (MissingElement t) = "Missing element: " ++ unpack t   data RssURI = forall a . RssURI (URIRef a)@@ -86,7 +90,7 @@   _ `compare` _ = GT  instance Show RssURI where-  show (RssURI a@URI{}) = show a+  show (RssURI a@URI{})         = show a   show (RssURI a@RelativeRef{}) = show a  withRssURI :: (forall a . URIRef a -> b) -> RssURI -> b
rss-conduit.cabal view
@@ -1,8 +1,8 @@ name:                rss-conduit-version:             0.2.0.2+version:             0.3.0.0 synopsis:            Streaming parser/renderer for the RSS 2.0 standard. description:         Cf README file.-license:             OtherLicense+license:             PublicDomain license-file:        LICENSE author:              koral maintainer:          koral att mailoo dott org@@ -25,20 +25,17 @@   build-depends:       base >= 4.8 && < 5     , conduit-    , conduit-parse+    , conduit-combinators     , containers-    , exceptions-    , foldl+    , safe-exceptions     , lens-simple     , mono-traversable-    , parsers     , safe     , text     , time >= 1.5     , timerep >= 2.0     , uri-bytestring >= 0.2     , xml-conduit >= 1.3-    , xml-conduit-parse >= 0.3     , xml-types   default-language: Haskell2010 @@ -53,13 +50,11 @@     , bytestring     , conduit     , conduit-extra-    , conduit-parse     , data-default-    , exceptions+    , safe-exceptions     , hlint     , lens-simple     , mono-traversable-    , parsers     , QuickCheck     , quickcheck-instances     , resourcet@@ -70,6 +65,5 @@     , text     , uri-bytestring >= 0.2     , xml-conduit >= 1.3-    , xml-conduit-parse >= 0.3     , xml-types   default-language: Haskell2010
test/Main.hs view
@@ -4,15 +4,13 @@ -- {{{ Imports import           Arbitrary -import           Control.Monad.Catch.Pure+import           Control.Exception.Safe       as Exception import           Control.Monad.Trans.Resource  import           Data.Char import           Data.Conduit import           Data.Conduit.Binary import           Data.Conduit.List-import           Data.Conduit.Parser-import           Data.Conduit.Parser.XML      as XML import           Data.Default import           Data.Version @@ -24,11 +22,11 @@ import           Test.Tasty.HUnit import           Test.Tasty.QuickCheck -import           Text.Parser.Combinators import           Text.RSS.Conduit.Parse       as Parser import           Text.RSS.Conduit.Render      as Renderer import           Text.RSS.Lens import           Text.RSS.Types+import           Text.XML.Stream.Parse        as XML hiding (choose) import           Text.XML.Stream.Render  import           URI.ByteString@@ -72,7 +70,7 @@  skipHoursCase :: TestTree skipHoursCase = testCase "<skipHours> element" $ do-  result <- runResourceT . runConduit $ sourceList input =$= XML.parseText def =$= runConduitParser rssSkipHours+  result <- runResourceT . runConduit $ sourceList input =$= XML.parseText' def =$= force "ERROR" rssSkipHours   result @?= [Hour 0, Hour 9, Hour 18, Hour 21]   where input = [ "<skipHours>"                 , "<hour>21</hour>"@@ -85,7 +83,7 @@  skipDaysCase :: TestTree skipDaysCase = testCase "<skipDays> element" $ do-  result <- runResourceT . runConduit $ sourceList input =$= XML.parseText def =$= runConduitParser rssSkipDays+  result <- runResourceT . runConduit $ sourceList input =$= XML.parseText' def =$= force "ERROR" rssSkipDays   result @?= [Monday, Saturday, Friday]   where input = [ "<skipDays>"                 , "<day>Monday</day>"@@ -97,7 +95,7 @@  textInputCase :: TestTree textInputCase = testCase "<textInput> element" $ do-  result <- runResourceT . runConduit $ sourceList input =$= XML.parseText def =$= runConduitParser rssTextInput+  result <- runResourceT . runConduit $ sourceList input =$= XML.parseText' def =$= force "ERROR" rssTextInput   result^.textInputTitleL @?= "Title"   result^.textInputDescriptionL @?= "Description"   result^.textInputNameL @?= "Name"@@ -112,7 +110,7 @@  imageCase :: TestTree imageCase = testCase "<image> element" $ do-  result <- runResourceT . runConduit $ sourceList input =$= XML.parseText def =$= runConduitParser rssImage+  result <- runResourceT . runConduit $ sourceList input =$= XML.parseText' def =$= force "ERROR" rssImage   result^.imageUriL @?= RssURI (URI (Scheme "http") (Just (Authority Nothing (Host "image.ext") Nothing)) "" (Query []) Nothing)   result^.imageTitleL @?= "Title"   result^.imageLinkL @?= RssURI (URI (Scheme "http") (Just (Authority Nothing (Host "link.ext") Nothing)) "" (Query []) Nothing)@@ -120,18 +118,20 @@   result^.imageHeightL @?= Just 200   result^.imageDescriptionL @?= "Description"   where input = [ "<image>"-                , "<uri>http://image.ext</uri>"+                , "<url>http://image.ext</url>"                 , "<title>Title</title>"+                , "<ignored>Ignored</ignored>"                 , "<link>http://link.ext</link>"                 , "<width>100</width>"                 , "<height>200</height>"                 , "<description>Description</description>"+                , "<ignored>Ignored</ignored>"                 , "</image>"                 ]  categoryCase :: TestTree categoryCase = testCase "<category> element" $ do-  result <- runResourceT . runConduit $ sourceList input =$= XML.parseText def =$= runConduitParser rssCategory+  result <- runResourceT . runConduit $ sourceList input =$= XML.parseText' def =$= force "ERROR" rssCategory   result @?= RssCategory "Domain" "Name"   where input = [ "<category domain=\"Domain\">"                 , "Name"@@ -140,7 +140,7 @@  cloudCase :: TestTree cloudCase = testCase "<cloud> element" $ do-  (result1, result2) <- runResourceT . runConduit $ sourceList input =$= XML.parseText def =$= runConduitParser ((,) <$> rssCloud <*> rssCloud)+  result1:result2:_ <- runResourceT . runConduit $ sourceList input =$= XML.parseText' def =$= XML.many rssCloud   result1 @?= RssCloud uri "pingMe" ProtocolSoap   result2 @?= RssCloud uri "myCloud.rssPleaseNotify" ProtocolXmlRpc   where input = [ "<cloud domain=\"rpc.sys.com\" port=\"80\" path=\"/RPC2\" registerProcedure=\"pingMe\" protocol=\"soap\"/>"@@ -150,7 +150,7 @@  guidCase :: TestTree guidCase = testCase "<guid> element" $ do-  result <- runResourceT . runConduit $ sourceList input =$= XML.parseText def =$= runConduitParser (some rssGuid)+  result <- runResourceT . runConduit $ sourceList input =$= XML.parseText' def =$= XML.many rssGuid   result @?= [GuidUri uri, GuidText "1", GuidText "2"]   where input = [ "<guid isPermaLink=\"true\">//guid.ext</guid>"                 , "<guid isPermaLink=\"false\">1</guid>"@@ -160,7 +160,7 @@  enclosureCase :: TestTree enclosureCase = testCase "<enclosure> element" $ do-  result <- runResourceT . runConduit $ sourceList input =$= XML.parseText def =$= runConduitParser rssEnclosure+  result <- runResourceT . runConduit $ sourceList input =$= XML.parseText' def =$= force "ERROR" rssEnclosure   result @?= RssEnclosure uri 12216320 "audio/mpeg"   where input = [ "<enclosure url=\"http://www.scripting.com/mp3s/weatherReportSuite.mp3\" length=\"12216320\" type=\"audio/mpeg\" />"                 ]@@ -168,7 +168,7 @@  sourceCase :: TestTree sourceCase = testCase "<source> element" $ do-  result <- runResourceT . runConduit $ sourceList input =$= XML.parseText def =$= runConduitParser rssSource+  result <- runResourceT . runConduit $ sourceList input =$= XML.parseText' def =$= force "ERROR" rssSource   result @?= RssSource uri "Tomalak's Realm"   where input = [ "<source url=\"http://www.tomalak.org/links2.xml\">Tomalak's Realm</source>"                 ]@@ -176,7 +176,7 @@  itemCase :: TestTree itemCase = testCase "<item> element" $ do-  result <- runResourceT . runConduit $ sourceList input =$= XML.parseText def =$= runConduitParser rssItem+  result <- runResourceT . runConduit $ sourceList input =$= XML.parseText' def =$= force "ERROR" rssItem   result^.itemTitleL @?= "Example entry"   result^.itemLinkL @?= Just link   result^.itemDescriptionL @?= "Here is some text containing an interesting description."@@ -195,7 +195,7 @@  documentCase :: TestTree documentCase = testCase "<rss> element" $ do-  result <- runResourceT . runConduit $ sourceList input =$= XML.parseText def =$= runConduitParser rssDocument+  result <- runResourceT . runConduit $ sourceList input =$= XML.parseText' def =$= force "ERROR" rssDocument   result^.documentVersionL @?= Version [2] []   result^.channelTitleL @?= "RSS Title"   result^.channelDescriptionL @?= "This is an example of an RSS feed"@@ -231,25 +231,25 @@   roundtripTextInputProperty :: TestTree-roundtripTextInputProperty = testProperty "parse . render = id (RssTextInput)" $ \t -> either (const False) (t ==) (runIdentity . runCatchT . runConduit $ renderRssTextInput t =$= runConduitParser rssTextInput)+roundtripTextInputProperty = testProperty "parse . render = id (RssTextInput)" $ \t -> either (const False) (t ==) (runConduit $ renderRssTextInput t =$= force "ERROR" rssTextInput)  roundtripImageProperty :: TestTree-roundtripImageProperty = testProperty "parse . render = id (RssImage)" $ \t -> either (const False) (t ==) (runIdentity . runCatchT . runConduit $ renderRssImage t =$= runConduitParser rssImage)+roundtripImageProperty = testProperty "parse . render = id (RssImage)" $ \t -> either (const False) (t ==) (runConduit $ renderRssImage t =$= force "ERROR" rssImage)  roundtripCategoryProperty :: TestTree-roundtripCategoryProperty = testProperty "parse . render = id (RssCategory)" $ \t -> either (const False) (t ==) (runIdentity . runCatchT . runConduit $ renderRssCategory t =$= runConduitParser rssCategory)+roundtripCategoryProperty = testProperty "parse . render = id (RssCategory)" $ \t -> either (const False) (t ==) (runConduit $ renderRssCategory t =$= force "ERROR" rssCategory)  roundtripEnclosureProperty :: TestTree-roundtripEnclosureProperty = testProperty "parse . render = id (RssEnclosure)" $ \t -> either (const False) (t ==) (runIdentity . runCatchT . runConduit $ renderRssEnclosure t =$= runConduitParser rssEnclosure)+roundtripEnclosureProperty = testProperty "parse . render = id (RssEnclosure)" $ \t -> either (const False) (t ==) (runConduit $ renderRssEnclosure t =$= force "ERROR" rssEnclosure)  roundtripSourceProperty :: TestTree-roundtripSourceProperty = testProperty "parse . render = id (RssSource)" $ \t -> either (const False) (t ==) (runIdentity . runCatchT . runConduit $ renderRssSource t =$= runConduitParser rssSource)+roundtripSourceProperty = testProperty "parse . render = id (RssSource)" $ \t -> either (const False) (t ==) (runConduit $ renderRssSource t =$= force "ERROR" rssSource)  roundtripGuidProperty :: TestTree-roundtripGuidProperty = testProperty "parse . render = id (RssGuid)" $ \t -> either (const False) (t ==) (runIdentity . runCatchT . runConduit $ renderRssGuid t =$= runConduitParser rssGuid)+roundtripGuidProperty = testProperty "parse . render = id (RssGuid)" $ \t -> either (const False) (t ==) (runConduit $ renderRssGuid t =$= force "ERROR" rssGuid)  roundtripItemProperty :: TestTree-roundtripItemProperty = testProperty "parse . render = id (RssItem)" $ \t -> either (const False) (t ==) (runIdentity . runCatchT . runConduit $ renderRssItem t =$= runConduitParser rssItem)+roundtripItemProperty = testProperty "parse . render = id (RssItem)" $ \t -> either (const False) (t ==) (runConduit $ renderRssItem t =$= force "ERROR" rssItem)   letter = choose ('a', 'z')