{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
-- | __Syndication__ module for RSS.
-- Cf specification at .
module Text.RSS.Extensions.Syndication
( -- * Types
SyndicationModule(..)
, RssChannelExtension(SyndicationChannel)
, RssItemExtension(SyndicationItem)
, SyndicationInfo(..)
, mkSyndicationInfo
, SyndicationPeriod(..)
, asSyndicationPeriod
-- * Parsers
, syndicationInfo
, syndicationPeriod
, syndicationFrequency
, syndicationBase
-- * Renderers
, renderSyndicationInfo
, renderSyndicationPeriod
, renderSyndicationFrequency
, renderSyndicationBase
-- * Misc
, namespacePrefix
, namespaceURI
) where
-- {{{ Imports
import Text.RSS.Extensions
import Text.RSS.Types
import Conduit hiding (throwM)
import Control.Applicative
import Control.Exception.Safe as Exception
import Control.Monad
import Control.Monad.Fix
import Data.Maybe
import Data.Singletons
import Data.Text
import Data.Time.Clock
import Data.Time.LocalTime
import Data.Time.RFC2822
import Data.Time.RFC3339
import Data.Time.RFC822
import Data.XML.Types
import GHC.Generics
import Lens.Simple
import Text.Read
import Text.XML.Stream.Parse
import qualified Text.XML.Stream.Render as Render
import URI.ByteString
-- }}}
-- {{{ Utils
tshow :: Show a => a -> Text
tshow = pack . show
asDate :: MonadThrow m => Text -> m UTCTime
asDate text = maybe (throw $ InvalidTime text) (return . zonedTimeToUTC) $
parseTimeRFC3339 text <|> parseTimeRFC2822 text <|> parseTimeRFC822 text
asInt :: MonadThrow m => Text -> m Int
asInt t = maybe (throwM $ InvalidInt t) return . readMaybe $ unpack t
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 ()
-- }}}
newtype SyndicationException = InvalidSyndicationPeriod Text deriving(Eq, Generic, Ord, Show)
instance Exception SyndicationException where
displayException (InvalidSyndicationPeriod t) = "Invalid syndication period: " ++ unpack t
-- | XML prefix is @sy@.
namespacePrefix :: Text
namespacePrefix = "sy"
-- | XML namespace is .
namespaceURI :: URIRef Absolute
namespaceURI = uri where Right uri = parseURI laxURIParserOptions "http://purl.org/rss/1.0/modules/syndication/"
syndicationName :: Text -> Name
syndicationName string = Name string (Just "http://purl.org/rss/1.0/modules/syndication/") (Just namespacePrefix)
syndicationTag :: MonadThrow m => Text -> ConduitM Event o m a -> ConduitM Event o m (Maybe a)
syndicationTag name = tagIgnoreAttrs (matching (== syndicationName name))
renderSyndicationTag :: Monad m => Text -> Text -> Source m Event
renderSyndicationTag name = Render.tag (syndicationName name) mempty . Render.content
data SyndicationPeriod = Hourly | Daily | Weekly | Monthly | Yearly
deriving (Bounded, Enum, Eq, Generic, Ord, Read, Show)
asSyndicationPeriod :: MonadThrow m => Text -> m SyndicationPeriod
asSyndicationPeriod "hourly" = pure Hourly
asSyndicationPeriod "daily" = pure Daily
asSyndicationPeriod "weekly" = pure Weekly
asSyndicationPeriod "monthly" = pure Monthly
asSyndicationPeriod "yearly" = pure Yearly
asSyndicationPeriod t = throw $ InvalidSyndicationPeriod t
fromSyndicationPeriod :: SyndicationPeriod -> Text
fromSyndicationPeriod Hourly = "hourly"
fromSyndicationPeriod Daily = "daily"
fromSyndicationPeriod Weekly = "weekly"
fromSyndicationPeriod Monthly = "monthly"
fromSyndicationPeriod Yearly = "yearly"
-- | __Syndication__ extension model.
data SyndicationInfo = SyndicationInfo
{ updatePeriod :: Maybe SyndicationPeriod
, updateFrequency :: Maybe Int
, updateBase :: Maybe UTCTime
} deriving (Eq, Generic, Ord, Read, Show)
-- | Construct an empty 'SyndicationInfo'.
mkSyndicationInfo :: SyndicationInfo
mkSyndicationInfo = SyndicationInfo mzero mzero mzero
data ElementPiece = ElementPeriod SyndicationPeriod | ElementFrequency Int | ElementBase UTCTime
makeTraversals ''ElementPiece
-- | Parse all __Syndication__ elements.
syndicationInfo :: MonadThrow m => ConduitM Event o m SyndicationInfo
syndicationInfo = manyYield' (choose piece) =$= parser where
parser = getZipConduit $ SyndicationInfo
<$> ZipConduit (projectC _ElementPeriod =$= headC)
<*> ZipConduit (projectC _ElementFrequency =$= headC)
<*> ZipConduit (projectC _ElementBase =$= headC)
piece = [ fmap ElementPeriod <$> syndicationPeriod
, fmap ElementFrequency <$> syndicationFrequency
, fmap ElementBase <$> syndicationBase
]
-- | Parse a @\@ element.
syndicationPeriod :: MonadThrow m => ConduitM Event o m (Maybe SyndicationPeriod)
syndicationPeriod = syndicationTag "updatePeriod" (content >>= asSyndicationPeriod)
-- | Parse a @\@ element.
syndicationFrequency :: MonadThrow m => ConduitM Event o m (Maybe Int)
syndicationFrequency = syndicationTag "updateFrequency" (content >>= asInt)
-- | Parse a @\@ element.
syndicationBase :: MonadThrow m => ConduitM Event o m (Maybe UTCTime)
syndicationBase = syndicationTag "updateBase" (content >>= asDate)
-- | Render all __Syndication__ elements.
renderSyndicationInfo :: Monad m => SyndicationInfo -> Source m Event
renderSyndicationInfo SyndicationInfo{..} = do
forM_ updatePeriod renderSyndicationPeriod
forM_ updateFrequency renderSyndicationFrequency
forM_ updateBase renderSyndicationBase
-- | Render a @\@ element.
renderSyndicationPeriod :: Monad m => SyndicationPeriod -> Source m Event
renderSyndicationPeriod = renderSyndicationTag "updatePeriod" . fromSyndicationPeriod
-- | Render a @\@ element.
renderSyndicationFrequency :: Monad m => Int -> Source m Event
renderSyndicationFrequency = renderSyndicationTag "updateFrequency" . tshow
-- | Render a @\@ element.
renderSyndicationBase :: Monad m => UTCTime -> Source m Event
renderSyndicationBase = renderSyndicationTag "updateBase" . formatTimeRFC822 . utcToZonedTime utc
-- | __Syndication__ tag type.
data SyndicationModule :: *
data instance Sing SyndicationModule = SSyndicationModule
instance SingI SyndicationModule where sing = SSyndicationModule
instance ParseRssExtension SyndicationModule where
parseRssChannelExtension = SyndicationChannel <$> syndicationInfo
parseRssItemExtension = pure SyndicationItem
instance RenderRssExtension SyndicationModule where
renderRssChannelExtension = renderSyndicationInfo . channelSyndicationInfo
renderRssItemExtension = const $ pure ()
data instance RssChannelExtension SyndicationModule = SyndicationChannel { channelSyndicationInfo :: SyndicationInfo}
deriving (Eq, Generic, Ord, Read, Show)
data instance RssItemExtension SyndicationModule = SyndicationItem deriving (Eq, Generic, Ord, Read, Show)