packages feed

servant-client-core-0.20.3.0: src/Servant/Client/Core/ServerSentEvents.hs

{-# LANGUAGE DeriveFunctor              #-}
{-# LANGUAGE DeriveGeneric              #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE ScopedTypeVariables        #-}
{-# OPTIONS_GHC -Wno-error=unused-imports #-}

-- | Server-sent events
--
-- See <https://www.w3.org/TR/2009/WD-eventsource-20090421/> for more details
-- on server-sent events (SSE).
--
module Servant.Client.Core.ServerSentEvents (
    EventMessage (..),
    EventIgnoreReason (..),
    Event (..),
    EventStreamT (..),
    JsonEventStreamT (..),
    EventMessageStreamT (..)
) where

import           Control.Applicative        (Alternative ((<|>)))
import           Control.Monad.IO.Class     (MonadIO)
import qualified Data.Aeson                 as Aeson
import qualified Data.Attoparsec.ByteString as Attoparsec
import qualified Data.ByteString            as ByteString
import qualified Data.ByteString.Char8      as ByteString.Char8
import qualified Data.ByteString.Lazy       as ByteString.Lazy
import           Data.Char                  (chr)
import           Data.Coerce                (coerce)
import           Data.Foldable              (traverse_)
import           Data.Functor               (void)
import qualified Data.Text                  as Text
import           Data.Text.Encoding         (encodeUtf8)
import           GHC.Generics               (Generic)
import           Numeric.Natural            (Natural)
import           Servant.API.ContentTypes   (EventStreamChunk (..))
import           Servant.API.Stream         (FromSourceIO (..))
import           Servant.Types.SourceT
                 (SourceT, StepT (..), foreachYieldStep, mapStepT,
                 transformStepWithAtto)

-- For compatibility with GHC <= 8.2
import           Data.Semigroup             (Semigroup (..))

-- | Line (or frame) of an event stream
newtype EventStreamLine = EventStreamLine
    { unEventStreamLine :: ByteString.ByteString }
    deriving Show

-- | Consume chunks to produce event stream lines.
eventLinesFromRawChunks
    :: Monad m
    => StepT m ByteString.ByteString
    -> StepT m EventStreamLine
eventLinesFromRawChunks =
    transformStepWithAtto eventLine

-- | Consume event stream chunks to produce event stream lines.
eventLinesFromChunks
    :: Monad m
    => StepT m EventStreamChunk
    -> StepT m EventStreamLine
eventLinesFromChunks =
    -- 'coerce' efficiently unpacks the 'EventStreamChunk'
    eventLinesFromRawChunks . fmap (coerce ByteString.Lazy.toStrict)

-- | Apply a 'Attoparsec.Parser' to each line of the event stream individually.
parseEventLines
    :: Monad m
    => Attoparsec.Parser a
    -> StepT m EventStreamLine
    -> StepT m a
parseEventLines parser =
    foreachYieldStep $ \(EventStreamLine line) next ->
        case Attoparsec.parseOnly parser line of
            Left err    -> Error err
            Right value -> Yield value next

-- | A line of an event stream
eventLine :: Attoparsec.Parser EventStreamLine
eventLine = do
    Attoparsec.option () byteOrderMark -- A line may be prefixed with a byte order mark
    EventStreamLine <$> untilLineEnd <* lineEnd

-- | Byte order mark (U+FEFF) in UTF-8 representation
byteOrderMark :: Attoparsec.Parser ()
byteOrderMark =
    traverse_ Attoparsec.word8
    $ ByteString.unpack
    $ encodeUtf8
    $ Text.singleton
    $ chr 0xFEFF

-- | Event stream line ending
lineEnd :: Attoparsec.Parser ()
lineEnd =
    (cr >> lf) <|> cr <|> lf <|> Attoparsec.endOfInput
    where
        cr = void (Attoparsec.word8 0x0D)
        lf = void (Attoparsec.word8 0x0A)

-- | Consume all contents until the end of the line.
untilLineEnd :: Attoparsec.Parser ByteString.ByteString
untilLineEnd = Attoparsec.takeWhile (\w8 -> w8 /= 0x0D && w8 /= 0x0A)

-- | Structured variant of an event line of an event stream
data EventMessage
    = EventDispatch
        -- ^ Dispatch on the accumulated event.
    | EventSetName ByteString.ByteString
        -- ^ Set the name of the current event.
    | EventSetLastId ByteString.ByteString
        -- ^ Set the last event identifier.
    | EventData ByteString.ByteString
        -- ^ Append data to the event's data buffer.
    | EventRetry Natural
        -- ^ Set the event stream's reconnection time.
    | EventIgnore EventIgnoreReason
        -- ^ Ignored
    deriving (Show, Eq, Ord)

-- | Reason why a event line can be ignored
data EventIgnoreReason
    = EventFieldNameUnknown ByteString.ByteString
    | EventRetryNonNumeric ByteString.ByteString
    | EventComment ByteString.ByteString
    deriving (Show, Eq, Ord)

-- | Parse the event stream lines into more structured messages.
eventMessagesFromLines
    :: Monad m
    => StepT m EventStreamLine
    -> StepT m EventMessage
eventMessagesFromLines =
    ensureLastDispatch False . parseEventLines eventMessage
    where
        -- | Make sure the last event message is a dispatch.
        ensureLastDispatch didDispatch step = case step of
            Stop ->
                if not didDispatch then Yield EventDispatch Stop else Stop
            Yield other next ->
                Yield other $ ensureLastDispatch (other == EventDispatch) next
            Skip next ->
                Skip $ ensureLastDispatch didDispatch next
            Effect eff ->
                Effect $ ensureLastDispatch didDispatch <$> eff
            err@Error{} ->
                 err

-- | Event line parser for an event message.
eventMessage :: Attoparsec.Parser EventMessage
eventMessage =
    ignore <|> field <|> dispatch
    where
        ignore = do
            _ <- Attoparsec.word8 0x3A -- ':'
            EventIgnore . EventComment <$> Attoparsec.takeByteString

        dispatch = do
            Attoparsec.endOfInput
            pure EventDispatch

        field = do
            name <- Attoparsec.takeWhile1 (/= 0x3A) -- Up to ':' or the end

            value <- Attoparsec.option ByteString.empty $ do
                _ <- Attoparsec.word8 0x3A -- ':'
                _ <- Attoparsec.option 0x20 $ Attoparsec.word8 0x20 -- Optional ' '
                Attoparsec.takeByteString

            pure $ case name of
                "event" -> EventSetName value

                "data" -> EventData value

                "id" -> EventSetLastId value

                "retry" ->
                    -- The retry value consist of digits.
                    if ByteString.all (\w8 -> 0x30 <= w8 && w8 <= 0x39) value then
                        EventRetry (read (ByteString.Char8.unpack value))
                    else
                        EventIgnore (EventRetryNonNumeric value)

                _ -> EventIgnore (EventFieldNameUnknown name)

-- | Event sent by the remote
data Event a = Event
    { eventName :: Maybe ByteString.ByteString
    , eventData :: a
    }
    deriving (Show, Eq, Ord, Functor, Generic)

-- | Accumulate event messages to build individual 'Event's.
eventsFromMessages
    :: Functor m
    => StepT m EventMessage
    -> StepT m (Event ByteString.ByteString)
eventsFromMessages =
    initGo
    where
        initGo = go Nothing ByteString.Lazy.empty

        combineData dataBuffer newData =
            if ByteString.Lazy.null dataBuffer then
                ByteString.Lazy.fromStrict newData
            else
                ByteString.Lazy.concat
                    [ dataBuffer
                    , ByteString.Lazy.singleton 0x0A -- Line feed
                    , ByteString.Lazy.fromStrict newData
                    ]

        go name dataBuffer step = case step of
            Stop ->
                Stop
            Skip next ->
                go name dataBuffer next
            Effect eff ->
                Effect (go name dataBuffer <$> eff)
            Error err ->
                Error err
            Yield message next -> case message of
                EventSetName newName ->
                    go (Just newName) dataBuffer next
                EventData newData ->
                    go name (combineData dataBuffer newData) next
                EventDispatch ->
                    Yield
                        (Event name (ByteString.Lazy.toStrict dataBuffer))
                        (initGo next)
                _ ->
                    -- We ignore other message because they don't fit into
                    -- the 'Event' type. If a user needs more fine grained
                    -- control, the 'EventMessage' interface is better suited.
                    go name dataBuffer next

-- | Server-sent event stream (SSE)
--
-- See <https://www.w3.org/TR/2009/WD-eventsource-20090421/> for more details.
--
newtype EventMessageStreamT m = EventMessageStreamT
    { unEventMessageStreamT :: SourceT m EventMessage }
    deriving stock (Show)
    deriving newtype (Semigroup, Monoid)

-- | Server-sent event messages
--
-- 'EventMessage' gives you more control over the communication with the server
-- than 'Event'.
--
instance MonadIO m => FromSourceIO EventStreamChunk (EventMessageStreamT m) where
    fromSourceIO src =
        EventMessageStreamT
        . mapStepT (eventMessagesFromLines . eventLinesFromChunks)
        <$> fromSourceIO src

-- | Server-sent event stream (SSE)
--
-- See <https://www.w3.org/TR/2009/WD-eventsource-20090421/> for more details.
--
newtype EventStreamT m = EventStreamT
    { unEventStreamT :: SourceT m (Event ByteString.ByteString) }
    deriving stock (Show)
    deriving newtype (Semigroup, Monoid)

-- | Server-sent events
instance MonadIO m => FromSourceIO EventStreamChunk (EventStreamT m) where
    fromSourceIO input = do
        src :: EventMessageStreamT m <- fromSourceIO input
        pure $
            -- 'coerce' is used in place of unpacking and repacking 'EventStreamT'
            coerce (mapStepT eventsFromMessages) src

-- | Try to parse event data to JSON.
jsonEventsFromEvents
    :: (Functor m, Aeson.FromJSON a)
    => StepT m (Event ByteString.ByteString)
    -> StepT m (Event a)
jsonEventsFromEvents =
    foreachYieldStep $ \(Event name datas) next ->
        either
            Error
            (\value -> Yield (Event name value) next)
            (Aeson.eitherDecode (ByteString.Lazy.fromStrict datas))

-- | Server-sent event stream (SSE) for JSON values
newtype JsonEventStreamT m a = JsonEventStreamT
    { unJsonEventStreamT :: SourceT m (Event a) }
    deriving stock (Show, Functor)
    deriving newtype (Semigroup, Monoid)

-- | Server-sent JSON event stream
instance (MonadIO m, Aeson.FromJSON a) => FromSourceIO EventStreamChunk (JsonEventStreamT m a) where
    fromSourceIO input = do
        src :: EventStreamT m <- fromSourceIO input
        pure $
            -- The 'coerce' efficiently unwraps the 'EventStreamT' and wraps the
            -- JsonEventStreamT.
            coerce
                (mapStepT jsonEventsFromEvents)
                src