hyperbole-0.6.0: src/Web/Hyperbole/Server/Message.hs
{-# LANGUAGE LambdaCase #-}
module Web.Hyperbole.Server.Message where
import Control.Applicative ((<|>))
import Control.Exception (Exception)
import Data.Aeson qualified as Aeson
import Data.Attoparsec.Text (Parser, char, endOfLine, isEndOfLine, parseOnly, sepBy, string, takeText, takeTill, takeWhile1)
import Data.ByteString qualified as BS
import Data.ByteString.Lazy qualified as BL
import Data.List qualified as L
import Data.String.Conversions (cs)
import Data.Text (Text)
import Data.Text qualified as T
import GHC.Generics (Generic)
import Web.Hyperbole.Data.Cookie (Cookie, Cookies)
import Web.Hyperbole.Data.Cookie qualified as Cookie
import Web.Hyperbole.Data.Encoded
import Web.Hyperbole.Data.QueryData (QueryData)
import Web.Hyperbole.Data.QueryData qualified as QueryData
import Web.Hyperbole.Data.URI (Path)
import Web.Hyperbole.Effect.Hyperbole (Remote (..))
import Web.Hyperbole.Types.Client (Client (..))
import Web.Hyperbole.Types.Event
import Web.Hyperbole.Types.Request
import Web.Hyperbole.View (ViewId)
{-
|UPDATE|
viewId: wahoo
action: hello
requestId: ipgeim
body
body
body
-}
data Message = Message
{ messageType :: Text
, event :: Event TargetViewId Encoded Encoded
, requestId :: RequestId
, metadata :: Metadata
, body :: MessageBody
}
deriving (Show)
newtype MessageBody = MessageBody {value :: BL.ByteString}
deriving newtype (Show)
data MessageError
= InvalidMessage String Text
| InvalidCookie BS.ByteString String
| MissingMeta String
deriving (Show, Exception)
-- Read Messages -------------------------------------
mimeType :: Text
mimeType = "application/hyperbole.message"
parseActionMessage :: Text -> Either String Message
parseActionMessage = parseOnly parser
where
parser :: Parser Message
parser = do
mt <- messageType
ev <- event
rq <- requestId
ms <- meta `sepBy` endOfLine
bd <- body
pure $ Message mt ev rq (mconcat ms) bd
messageType :: Parser Text
messageType = do
_ <- char '|'
t <- takeWhile1 (/= '|')
_ <- char '|'
endOfLine
pure t
body :: Parser MessageBody
body = do
MessageBody . cs . T.strip <$> takeText
event :: Parser (Event TargetViewId Encoded Encoded)
event = do
vid <- targetViewId
act <- encodedAction
st <- encodedState <|> pure mempty
pure $ Event vid act st
where
targetViewId :: Parser TargetViewId
targetViewId = do
_ <- string "ViewId: "
line <- takeLine
v <- case encodedParseText line of
Left e -> fail $ "Parse Encoded ViewId failed: " <> cs e <> " from " <> cs line
Right a -> pure $ TargetViewId a
endOfLine
pure v
encodedAction :: Parser Encoded
encodedAction = do
_ <- string "Action: "
inp <- takeLine
v <- case encodedParseText inp of
Left e -> fail $ "Parse Encoded ViewAction failed: " <> cs e <> " from " <> cs inp
Right a -> pure a
endOfLine
pure v
encodedState :: Parser Encoded
encodedState = do
_ <- string "State: "
inp <- takeLine
v <- case encodedParseText inp of
Left e -> fail $ "Parse Encoded ViewState failed: " <> cs e <> " from " <> cs inp
Right a -> pure a
endOfLine
pure v
requestId :: Parser RequestId
requestId = do
_ <- string "RequestId: "
r <- RequestId <$> takeLine
endOfLine
pure r
meta :: Parser Metadata
meta = do
key <- metaKey
value <- takeLine
pure $ metadata (cs key) value
metaKey :: Parser MetaKey
metaKey = do
key <- takeWhile1 (/= ':')
_ <- string ": "
pure key
takeLine :: Parser Text
takeLine = do
takeTill isEndOfLine
-- Render ---------------------------------------------
renderMetadata :: Metadata -> Text
renderMetadata (Metadata m) = T.intercalate "\n" $ fmap (uncurry metaLine) m
metaLine :: MetaKey -> Text -> Text
metaLine name value = name <> ": " <> cs value
-- Metadata --------------------------------------------
type MetaKey = Text
newtype Metadata = Metadata [(Text, Text)]
deriving newtype (Semigroup, Monoid)
deriving (Show, Generic)
deriving anyclass (ViewId)
-- instance HyperView Metadata es where
-- data Action Metadata = MetaNone
-- deriving (Generic, ViewAction)
-- update _ = pure none
metadata :: MetaKey -> Text -> Metadata
metadata key value = Metadata [(key, value)]
lookupMetadata :: MetaKey -> Metadata -> Maybe Text
lookupMetadata key (Metadata kvs) = L.lookup key kvs
requestMetadata :: Request -> Metadata
requestMetadata req =
maybe mempty eventMetadata req.event <> metaRequestId req.requestId
where
metaRequestId :: RequestId -> Metadata
metaRequestId (RequestId "") = mempty
metaRequestId (RequestId reqId) =
metadata "RequestId" (cs reqId)
eventMetadata :: Event TargetViewId Encoded Encoded -> Metadata
eventMetadata event =
Metadata
[ ("ViewId", encodedToText event.viewId.encoded)
, ("Action", encodedToText event.action)
]
targetViewMetadata :: TargetViewId -> Metadata
targetViewMetadata (TargetViewId vid) = Metadata [("TargetViewId", encodedToText vid)]
responseMetadata :: Path -> Client -> [Remote] -> Metadata
responseMetadata reqPath client remotes =
clientMetadata reqPath client <> metaRemotes remotes
clientMetadata :: Path -> Client -> Metadata
clientMetadata reqPath client =
metaSession client.session <> metaQuery client.query <> metaPageTitle client.pageTitle
where
metaPageTitle :: Maybe Text -> Metadata
metaPageTitle = \case
Nothing -> mempty
Just pt -> metadata "PageTitle" pt
metaQuery :: Maybe QueryData -> Metadata
metaQuery Nothing = mempty
metaQuery (Just q) =
Metadata [("Query", cs $ QueryData.render q)]
metaSession :: Cookies -> Metadata
metaSession cookies = mconcat $ fmap metaCookie $ Cookie.toList cookies
where
metaCookie :: Cookie -> Metadata
metaCookie cookie =
Metadata [("Cookie", cs (Cookie.render reqPath cookie))]
metaRemotes :: [Remote] -> Metadata
metaRemotes rs = mconcat $ fmap meta rs
where
meta = \case
RemoteAction (TargetViewId vid) act ->
metadata "Trigger" $ encodedToText vid <> "|" <> encodedToText act
RemoteEvent ev dat ->
metadata "Event" $ T.intercalate "|" [ev, cs $ Aeson.encode dat]
metaError :: Text -> Metadata
metaError = metadata "Error"
-- metaRedirect :: URI -> Metadata
-- metaRedirect u = metadata "Redirect" (uriToText u)
data ContentType
= ContentHtml
| ContentText
data RenderedMessage
= MessageHtml BL.ByteString
| MessageText Text