rss-conduit 0.3.0.1 → 0.3.1.0
raw patch · 4 files changed
+401/−17 lines, 4 filesdep −conduit-extraPVP ok
version bump matches the API change (PVP)
Dependencies removed: conduit-extra
API changes (from Hackage documentation)
+ Text.RSS1.Conduit.Parse: rss1ChannelItems :: MonadThrow m => ConduitM Event o m (Maybe [Text])
+ Text.RSS1.Conduit.Parse: rss1Document :: MonadThrow m => ConduitM Event o m (Maybe RssDocument)
+ Text.RSS1.Conduit.Parse: rss1Image :: (MonadThrow m) => ConduitM Event o m (Maybe RssImage)
+ Text.RSS1.Conduit.Parse: rss1Item :: MonadThrow m => ConduitM Event o m (Maybe RssItem)
+ Text.RSS1.Conduit.Parse: rss1TextInput :: (MonadThrow m) => ConduitM Event o m (Maybe RssTextInput)
Files
- README.md +2/−1
- Text/RSS1/Conduit/Parse.hs +254/−0
- rss-conduit.cabal +4/−3
- test/Main.hs +141/−13
README.md view
@@ -1,10 +1,11 @@ # rss-conduit -This [Haskell][hsk] library implements a streaming parser/renderer for the [RSS 2.0 syndication format][rss], based on [conduit][cdt]s.+This [Haskell][hsk] library implements a streaming parser/renderer for the [RSS 2.0 syndication format][rss], and a streaming parser for the [RSS 1.0 syndication format][rss1], based on [conduit][cdt]s. Parsers are lenient as much as possible. E.g. unexpected tags are simply ignored. [rss]: http://cyber.law.harvard.edu/rss/rss.html+[rss1]: http://web.resource.org/rss/1.0/spec [cdt]: https://hackage.haskell.org/package/conduit [hsk]: https://haskell.org
+ Text/RSS1/Conduit/Parse.hs view
@@ -0,0 +1,254 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE TemplateHaskell #-}+-- | Streaming parsers for the RSS 1.0 standard.+module Text.RSS1.Conduit.Parse+ ( -- * Top-level+ rss1Document+ -- * Elements+ , rss1ChannelItems+ , rss1Image+ , rss1Item+ , rss1TextInput+ ) where++-- {{{ Imports+import Text.RSS.Types++import Conduit hiding (throwM)++import Control.Exception.Safe as Exception+import Control.Monad+import Control.Monad.Fix++import Data.Conduit+import Data.Text as Text+import Data.Text.Encoding+import Data.Time.Clock+import Data.Time.LocalTime+import Data.Time.RFC3339+import Data.Version+import Data.XML.Types++import Lens.Simple++import Text.XML.Stream.Parse++import URI.ByteString+-- }}}++-- {{{ Util+asDate :: (MonadThrow m) => Text -> m UTCTime+asDate text = maybe (throw $ InvalidTime text) (return . zonedTimeToUTC) $ parseTimeRFC3339 text++asRssURI :: (MonadThrow m) => Text -> m RssURI+asRssURI t = case (parseURI' t, parseRelativeRef' t) of+ (Right u, _) -> return $ RssURI u+ (_, Right u) -> return $ RssURI u+ (_, Left e) -> throwM $ InvalidURI e+ where parseURI' = parseURI laxURIParserOptions . encodeUtf8+ parseRelativeRef' = parseRelativeRef laxURIParserOptions . encodeUtf8++nullURI :: RssURI+nullURI = RssURI $ RelativeRef Nothing "" (Query []) Nothing++headRequiredC :: MonadThrow m => Text -> Consumer a m a+headRequiredC e = maybe (throw $ MissingElement e) return =<< headC++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 ()+++contentTag :: MonadThrow m => Text -> AttrParser a -> (a -> ConduitM Event o m b) -> ConduitM Event o m (Maybe b)+contentTag string = tag' (matching (== contentName string))++dcTag :: MonadThrow m => Text -> AttrParser a -> (a -> ConduitM Event o m b) -> ConduitM Event o m (Maybe b)+dcTag string = tag' (matching (== dcName string))++rdfTag :: MonadThrow m => Text -> AttrParser a -> (a -> ConduitM Event o m b) -> ConduitM Event o m (Maybe b)+rdfTag string = tag' (matching (== rdfName string))++rss1Tag :: MonadThrow m => Text -> AttrParser a -> (a -> ConduitM Event o m b) -> ConduitM Event o m (Maybe b)+rss1Tag string = tag' (matching (== rss1Name string))++contentName :: Text -> Name+contentName string = Name string (Just "http://purl.org/rss/1.0/modules/content/") (Just "content")++dcName :: Text -> Name+dcName string = Name string (Just "http://purl.org/dc/elements/1.1/") (Just "dc")++rdfName :: Text -> Name+rdfName string = Name string (Just "http://www.w3.org/1999/02/22-rdf-syntax-ns#") (Just "rdf")++rss1Name :: Text -> Name+rss1Name string = Name string (Just "http://purl.org/rss/1.0/") Nothing+-- }}}+++data TextInputPiece = TextInputTitle Text | TextInputDescription Text+ | TextInputName Text | TextInputLink RssURI++makeTraversals ''TextInputPiece++-- | Parse a @\<textinput\>@ element.+rss1TextInput :: (MonadThrow m) => ConduitM Event o m (Maybe RssTextInput)+rss1TextInput = rss1Tag "textinput" attributes $ \uri -> (manyYield' (choose piece) =$= parser uri) <* many ignoreAnyTreeContent where+ parser uri = 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 =$= headDefC uri) -- Lenient+ piece = [ fmap TextInputTitle <$> rss1Tag "title" ignoreAttrs (const content)+ , fmap TextInputDescription <$> rss1Tag "description" ignoreAttrs (const content)+ , fmap TextInputName <$> rss1Tag "name" ignoreAttrs (const content)+ , fmap TextInputLink <$> rss1Tag "link" ignoreAttrs (const $ content >>= asRssURI)+ ]+ attributes = (requireAttr (rdfName "about") >>= asRssURI) <* ignoreAttrs+++data ItemPiece = ItemTitle Text | ItemLink RssURI | ItemDescription Text | ItemCreator Text | ItemDate UTCTime | ItemContent Text++makeTraversals ''ItemPiece++-- | Parse an @\<item\>@ element.+rss1Item :: MonadThrow m => ConduitM Event o m (Maybe RssItem)+rss1Item = rss1Tag "item" attributes $ \uri -> (manyYield' (choose piece) =$= parser uri) <* many ignoreAnyTreeContent where+ parser uri = getZipConduit $ RssItem+ <$> ZipConduit (projectC _ItemTitle =$= headDefC mempty)+ <*> (Just <$> ZipConduit (projectC _ItemLink =$= headDefC uri))+ <*> ZipConduit (projectC _ItemDescription =$= headDefC mempty)+ <*> ZipConduit (projectC _ItemCreator =$= headDefC mempty)+ <*> pure mempty+ <*> pure mzero+ <*> pure mempty+ <*> pure mzero+ <*> ZipConduit (projectC _ItemDate =$= headC)+ <*> pure mzero+ piece = [ fmap ItemTitle <$> rss1Tag "title" ignoreAttrs (const content)+ , fmap ItemLink <$> rss1Tag "link" ignoreAttrs (const $ content >>= asRssURI)+ , fmap ItemDescription <$> (rss1Tag "description" ignoreAttrs (const content) `orE` contentTag "encoded" ignoreAttrs (const content))+ , fmap ItemCreator <$> dcTag "creator" ignoreAttrs (const content)+ , fmap ItemDate <$> dcTag "date" ignoreAttrs (const $ content >>= asDate)+ ]+ attributes = (requireAttr (rdfName "about") >>= asRssURI) <* ignoreAttrs+++data ImagePiece = ImageUri RssURI | ImageTitle Text | ImageLink RssURI++makeTraversals ''ImagePiece++-- | Parse an @\<image\>@ element.+rss1Image :: (MonadThrow m) => ConduitM Event o m (Maybe RssImage)+rss1Image = rss1Tag "image" attributes $ \uri -> (manyYield' (choose piece) =$= parser uri) <* many ignoreAnyTreeContent where+ parser uri = getZipConduit $ RssImage+ <$> ZipConduit (projectC _ImageUri =$= headDefC uri) -- Lenient+ <*> ZipConduit (projectC _ImageTitle =$= headDefC "Unnamed image") -- Lenient+ <*> ZipConduit (projectC _ImageLink =$= headDefC nullURI) -- Lenient+ <*> pure mzero+ <*> pure mzero+ <*> pure mempty+ piece = [ fmap ImageUri <$> rss1Tag "url" ignoreAttrs (const $ content >>= asRssURI)+ , fmap ImageTitle <$> rss1Tag "title" ignoreAttrs (const content)+ , fmap ImageLink <$> rss1Tag "link" ignoreAttrs (const $ content >>= asRssURI)+ ]+ attributes = (requireAttr (rdfName "about") >>= asRssURI) <* ignoreAttrs+++-- | Parse an @\<items\>@ element.+rss1ChannelItems :: MonadThrow m => ConduitM Event o m (Maybe [Text])+rss1ChannelItems = fmap join $ rss1Tag "items" ignoreAttrs $ const $ rdfTag "Seq" ignoreAttrs $ const $ many $ rdfTag "li" attributes return where+ attributes = requireAttr (rdfName "resource") <* ignoreAttrs+++data Rss1Channel = Rss1Channel+ { channelId' :: RssURI+ , channelTitle' :: Text+ , channelLink' :: RssURI+ , channelDescription' :: Text+ , channelItems' :: [Text]+ , channelImage' :: Maybe RssImage+ , channelTextInput' :: Maybe RssURI+ }++data ChannelPiece = ChannelTitle Text+ | ChannelLink RssURI+ | ChannelDescription Text+ | ChannelImage RssImage+ | ChannelItems [Text]+ | ChannelTextInput RssURI++makeTraversals ''ChannelPiece+++-- | Parse a @\<channel\>@ element.+rss1Channel :: MonadThrow m => ConduitM Event o m (Maybe Rss1Channel)+rss1Channel = rss1Tag "channel" attributes $ \channelId -> (manyYield' (choose piece) =$= parser channelId) <* many ignoreAnyTreeContent where+ parser channelId = getZipConduit $ Rss1Channel channelId+ <$> ZipConduit (projectC _ChannelTitle =$= headRequiredC "Missing <title> element")+ <*> ZipConduit (projectC _ChannelLink =$= headRequiredC "Missing <link> element")+ <*> ZipConduit (projectC _ChannelDescription =$= headDefC "") -- Lenient+ <*> ZipConduit (projectC _ChannelItems =$= concatC =$= sinkList)+ <*> ZipConduit (projectC _ChannelImage =$= headC)+ <*> ZipConduit (projectC _ChannelTextInput =$= headC)+ piece = [ fmap ChannelTitle <$> rss1Tag "title" ignoreAttrs (const content)+ , fmap ChannelLink <$> rss1Tag "link" ignoreAttrs (const $ content >>= asRssURI)+ , fmap ChannelDescription <$> rss1Tag "description" ignoreAttrs (const content)+ , fmap ChannelItems <$> rss1ChannelItems+ , fmap ChannelImage <$> rss1Image+ , fmap ChannelTextInput <$> rss1Tag "textinput" (requireAttr (rdfName "resource") >>= asRssURI) return+ ]+ attributes = (requireAttr (rdfName "about") >>= asRssURI) <* ignoreAttrs+++data Rss1Document = Rss1Document Rss1Channel (Maybe RssImage) [RssItem] (Maybe RssTextInput)++rss1ToRss2 :: Rss1Document -> RssDocument+rss1ToRss2 (Rss1Document channel image items textInput) = RssDocument+ (Version [1] [])+ (channelTitle' channel)+ (channelLink' channel)+ (channelDescription' channel)+ items+ mempty+ mempty+ mempty+ mempty+ mzero+ mzero+ mzero+ mempty+ mzero+ mzero+ mzero+ image+ mempty+ textInput+ mempty+ mempty++data DocumentPiece = DocumentChannel Rss1Channel+ | DocumentImage RssImage+ | DocumentItem RssItem+ | DocumentTextInput RssTextInput++makeTraversals ''DocumentPiece+++-- | Parse an @\<RDF\>@ element.+rss1Document :: MonadThrow m => ConduitM Event o m (Maybe RssDocument)+rss1Document = fmap (fmap rss1ToRss2) $ rdfTag "RDF" ignoreAttrs $ const $ (manyYield' (choose piece) =$= parser) <* many ignoreAnyTreeContent where+ parser = getZipConduit $ Rss1Document+ <$> ZipConduit (projectC _DocumentChannel =$= headRequiredC "Missing <channel> element")+ <*> ZipConduit (projectC _DocumentImage =$= headC)+ <*> ZipConduit (projectC _DocumentItem =$= sinkList)+ <*> ZipConduit (projectC _DocumentTextInput =$= headC)+ piece = [ fmap DocumentChannel <$> rss1Channel+ , fmap DocumentImage <$> rss1Image+ , fmap DocumentItem <$> rss1Item+ , fmap DocumentTextInput <$> rss1TextInput+ ]
rss-conduit.cabal view
@@ -1,6 +1,6 @@ name: rss-conduit-version: 0.3.0.1-synopsis: Streaming parser/renderer for the RSS 2.0 standard.+version: 0.3.1.0+synopsis: Streaming parser/renderer for the RSS standard. description: Cf README file. license: PublicDomain license-file: LICENSE@@ -17,6 +17,7 @@ library exposed-modules:+ Text.RSS1.Conduit.Parse Text.RSS.Conduit.Parse Text.RSS.Conduit.Render Text.RSS.Lens@@ -49,7 +50,7 @@ , base >= 4.8 && < 5 , bytestring , conduit- , conduit-extra+ , conduit-combinators , data-default , safe-exceptions , hlint
test/Main.hs view
@@ -4,12 +4,13 @@ -- {{{ Imports import Arbitrary +import Conduit+ 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.Default import Data.Version@@ -26,6 +27,7 @@ import Text.RSS.Conduit.Render as Renderer import Text.RSS.Lens import Text.RSS.Types+import Text.RSS1.Conduit.Parse as Parser import Text.XML.Stream.Parse as XML hiding (choose) import Text.XML.Stream.Render @@ -45,15 +47,20 @@ unitTests = testGroup "Unit tests" [ skipHoursCase , skipDaysCase- , textInputCase- , imageCase+ , rss1TextInputCase+ , rss2TextInputCase+ , rss1ImageCase+ , rss2ImageCase , categoryCase , cloudCase , guidCase , enclosureCase , sourceCase- , itemCase- , documentCase+ , rss1ItemCase+ , rss2ItemCase+ , rss1ChannelItemsCase+ , rss1DocumentCase+ , rss2DocumentCase ] properties :: TestTree@@ -93,8 +100,23 @@ , "</skipDays>" ] -textInputCase :: TestTree-textInputCase = testCase "<textInput> element" $ do+rss1TextInputCase :: TestTree+rss1TextInputCase = testCase "RSS1 <textinput> element" $ do+ result <- runResourceT . runConduit $ sourceList input =$= XML.parseText' def =$= force "ERROR" rss1TextInput+ result^.textInputTitleL @?= "Search XML.com"+ result^.textInputDescriptionL @?= "Search XML.com's XML collection"+ result^.textInputNameL @?= "s"+ result^.textInputLinkL @=? RssURI (URI (Scheme "http") (Just (Authority Nothing (Host "search.xml.com") Nothing)) "" (Query []) Nothing)+ where input = [ "<textinput xmlns=\"http://purl.org/rss/1.0/\" xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" rdf:about=\"http://search.xml.com\">"+ , "<title>Search XML.com</title>"+ , "<description>Search XML.com's XML collection</description>"+ , "<name>s</name>"+ , "<link>http://search.xml.com</link>"+ , "</textinput>"+ ]++rss2TextInputCase :: TestTree+rss2TextInputCase = testCase "RSS2 <textInput> element" $ do result <- runResourceT . runConduit $ sourceList input =$= XML.parseText' def =$= force "ERROR" rssTextInput result^.textInputTitleL @?= "Title" result^.textInputDescriptionL @?= "Description"@@ -108,8 +130,23 @@ , "</textInput>" ] -imageCase :: TestTree-imageCase = testCase "<image> element" $ do+rss1ImageCase :: TestTree+rss1ImageCase = testCase "RSS1 <image> element" $ do+ result <- runResourceT . runConduit $ sourceList input =$= XML.parseText' def =$= force "ERROR" rss1Image+ result^.imageUriL @?= RssURI (URI (Scheme "http") (Just (Authority Nothing (Host "xml.com") Nothing)) "/universal/images/xml_tiny.gif" (Query []) Nothing)+ result^.imageTitleL @?= "XML.com"+ result^.imageLinkL @?= RssURI (URI (Scheme "http") (Just (Authority Nothing (Host "www.xml.com") Nothing)) "" (Query []) Nothing)+ where input = [ "<image xmlns=\"http://purl.org/rss/1.0/\" xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" rdf:about=\"http://xml.com/universal/images/xml_tiny.gif\">"+ , "<url>http://xml.com/universal/images/xml_tiny.gif</url>"+ , "<title>XML.com</title>"+ , "<ignored>Ignored</ignored>"+ , "<link>http://www.xml.com</link>"+ , "<ignored>Ignored</ignored>"+ , "</image>"+ ]++rss2ImageCase :: TestTree+rss2ImageCase = testCase "RSS2 <image> element" $ do 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"@@ -174,8 +211,26 @@ ] uri = RssURI (URI (Scheme "http") (Just (Authority Nothing (Host "www.tomalak.org") Nothing)) "/links2.xml" (Query []) Nothing) -itemCase :: TestTree-itemCase = testCase "<item> element" $ do+rss1ItemCase :: TestTree+rss1ItemCase = testCase "RSS1 <item> element" $ do+ result <- runResourceT . runConduit $ sourceList input =$= XML.parseText' def =$= force "ERROR" rss1Item+ result^.itemTitleL @?= "Processing Inclusions with XSLT"+ result^.itemLinkL @?= Just link+ result^.itemDescriptionL @?= "Processing document inclusions with general XML tools can be problematic. This article proposes a way of preserving inclusion information through SAX-based processing."+ where input = [ "<item xmlns=\"http://purl.org/rss/1.0/\" xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" rdf:about=\"http://xml.com/pub/2000/08/09/xslt/xslt.html\">"+ , "<title>Processing Inclusions with XSLT</title>"+ , "<description>Processing document inclusions with general XML tools can be"+ , " problematic. This article proposes a way of preserving inclusion"+ , " information through SAX-based processing."+ , "</description>"+ , "<link>http://xml.com/pub/2000/08/09/xslt/xslt.html</link>"+ , "<sometag>Some content in unknown tag, should be ignored.</sometag>"+ , "</item>"+ ]+ link = RssURI (URI (Scheme "http") (Just (Authority Nothing (Host "xml.com") Nothing)) "/pub/2000/08/09/xslt/xslt.html" (Query []) Nothing)++rss2ItemCase :: TestTree+rss2ItemCase = testCase "RSS2 <item> element" $ do result <- runResourceT . runConduit $ sourceList input =$= XML.parseText' def =$= force "ERROR" rssItem result^.itemTitleL @?= "Example entry" result^.itemLinkL @?= Just link@@ -193,8 +248,81 @@ ] link = RssURI (URI (Scheme "http") (Just (Authority Nothing (Host "www.example.com") Nothing)) "/blog/post/1" (Query []) Nothing) -documentCase :: TestTree-documentCase = testCase "<rss> element" $ do++rss1ChannelItemsCase :: TestTree+rss1ChannelItemsCase = testCase "RSS1 <items> element" $ do+ result <- runResourceT . runConduit $ sourceList input =$= XML.parseText' def =$= force "ERROR" rss1ChannelItems+ result @?= [resource1, resource2]+ where input = [ "<items xmlns=\"http://purl.org/rss/1.0/\" xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">"+ , "<rdf:Seq>"+ , "<rdf:li rdf:resource=\"http://xml.com/pub/2000/08/09/xslt/xslt.html\" />"+ , "<rdf:li rdf:resource=\"http://xml.com/pub/2000/08/09/rdfdb/index.html\" />"+ , "</rdf:Seq>"+ , "</items>"+ ]+ resource1 = "http://xml.com/pub/2000/08/09/xslt/xslt.html"+ resource2 = "http://xml.com/pub/2000/08/09/rdfdb/index.html"++rss1DocumentCase :: TestTree+rss1DocumentCase = testCase "<rdf> element" $ do+ result <- runResourceT . runConduit $ sourceList input =$= XML.parseText' def =$= force "ERROR" rss1Document+ result^.documentVersionL @?= Version [1] []+ result^.channelTitleL @?= "XML.com"+ result^.channelDescriptionL @?= "XML.com features a rich mix of information and services for the XML community."+ result^.channelLinkL @?= link+ result^?channelImageL._Just.imageTitleL @?= Just "XML.com"+ result^?channelImageL._Just.imageLinkL @?= Just imageLink+ result^?channelImageL._Just.imageUriL @?= Just imageUri+ length (result^..channelItemsL) @?= 2+ result^?channelTextInputL._Just.textInputTitleL @?= Just "Search XML.com"+ result^?channelTextInputL._Just.textInputDescriptionL @?= Just "Search XML.com's XML collection"+ result^?channelTextInputL._Just.textInputNameL @?= Just "s"+ result^?channelTextInputL._Just.textInputLinkL @?= Just textInputLink+ where input = [ "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"+ , "<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns=\"http://purl.org/rss/1.0/\">"+ , "<channel rdf:about=\"http://www.xml.com/xml/news.rss\">"+ , "<title>XML.com</title>"+ , "<link>http://xml.com/pub</link>"+ , "<description>XML.com features a rich mix of information and services for the XML community.</description>"+ , "<image rdf:resource=\"http://xml.com/universal/images/xml_tiny.gif\" />"+ , "<items>"+ , "<rdf:Seq>"+ , "<rdf:li rdf:resource=\"http://xml.com/pub/2000/08/09/xslt/xslt.html\" />"+ , "<rdf:li rdf:resource=\"http://xml.com/pub/2000/08/09/rdfdb/index.html\" />"+ , "</rdf:Seq>"+ , "</items>"+ , "</channel>"+ , "<image rdf:about=\"http://xml.com/universal/images/xml_tiny.gif\">"+ , "<title>XML.com</title>"+ , "<link>http://www.xml.com</link>"+ , "<url>http://xml.com/universal/images/xml_tiny.gif</url>"+ , "</image>"+ , "<item rdf:about=\"http://xml.com/pub/2000/08/09/xslt/xslt.html\">"+ , "<title>Processing Inclusions with XSLT</title>"+ , "<link>http://xml.com/pub/2000/08/09/xslt/xslt.html</link>"+ , "<description>Processing document inclusions with general XML tools can be problematic. This article proposes a way of preserving inclusion information through SAX-based processing.</description>"+ , "</item>"+ , "<item rdf:about=\"http://xml.com/pub/2000/08/09/xslt/xslt.html\">"+ , "<title>Putting RDF to Work</title>"+ , "<link>http://xml.com/pub/2000/08/09/rdfdb/index.html</link>"+ , "<description>Tool and API support for the Resource Description Framework is slowly coming of age. Edd Dumbill takes a look at RDFDB, one of the most exciting new RDF toolkits.</description>"+ , "</item>"+ , "<textinput rdf:about=\"http://search.xml.com\">"+ , "<title>Search XML.com</title>"+ , "<description>Search XML.com's XML collection</description>"+ , "<name>s</name>"+ , "<link>http://search.xml.com</link>"+ , "</textinput>"+ , "</rdf:RDF>"+ ]+ link = RssURI (URI (Scheme "http") (Just (Authority Nothing (Host "xml.com") Nothing)) "/pub" (Query []) Nothing)+ imageLink = RssURI (URI (Scheme "http") (Just (Authority Nothing (Host "www.xml.com") Nothing)) "" (Query []) Nothing)+ imageUri = RssURI (URI (Scheme "http") (Just (Authority Nothing (Host "xml.com") Nothing)) "/universal/images/xml_tiny.gif" (Query []) Nothing)+ textInputLink = RssURI (URI (Scheme "http") (Just (Authority Nothing (Host "search.xml.com") Nothing)) "" (Query []) Nothing)+++rss2DocumentCase :: TestTree+rss2DocumentCase = testCase "<rss> element" $ do result <- runResourceT . runConduit $ sourceList input =$= XML.parseText' def =$= force "ERROR" rssDocument result^.documentVersionL @?= Version [2] [] result^.channelTitleL @?= "RSS Title"