sparrow-0.0.2.2: src/Web/Dependencies/Sparrow/Types.hs
{-# LANGUAGE
DeriveGeneric
, GeneralizedNewtypeDeriving
, OverloadedStrings
, RecordWildCards
, NamedFieldPuns
, RankNTypes
#-}
module Web.Dependencies.Sparrow.Types where
import Web.Dependencies.Sparrow.Session (SessionID)
import Data.Hashable (Hashable)
import Data.Text (Text, intercalate, unpack)
import qualified Data.Text as T
import qualified Data.Text.Lazy.Encoding as LT
import qualified Data.ByteString.Lazy as LBS
import Data.Aeson (ToJSON (..), FromJSON (..), Value (String, Object), (.=), object, (.:))
import Data.Aeson.Types (typeMismatch)
import Data.Aeson.Attoparsec (attoAeson)
import Data.Aeson.JSONVoid (JSONVoid)
import Data.String (IsString (..))
import Data.Attoparsec.Text (Parser, takeWhile1, char, sepBy)
import Control.Applicative (Alternative (empty), (<|>))
import Control.DeepSeq (NFData)
import Control.Monad.IO.Class (MonadIO (liftIO))
import Control.Concurrent.Async (Async)
import Control.Concurrent.STM (TVar, newTVarIO)
import GHC.Generics (Generic)
-- * Conceptual
-- ** Server
data ServerArgs m deltaOut = ServerArgs
{ serverDeltaReject :: m ()
, serverSendCurrent :: deltaOut -> m ()
}
hoistServerArgs :: (forall a. m a -> n a)
-> ServerArgs m deltaOut
-> ServerArgs n deltaOut
hoistServerArgs f ServerArgs{..} = ServerArgs
{ serverDeltaReject = f serverDeltaReject
, serverSendCurrent = f . serverSendCurrent
}
data ServerReturn m f initOut deltaIn deltaOut = ServerReturn
{ serverInitOut :: initOut
, serverOnOpen :: ServerArgs m deltaOut
-> m (TVar (f (Async ())))
-- ^ invoked once, and should return a 'Control.Concurrent.Async.link'ed long-lived thread
-- to kill when the subscription dies
, serverOnReceive :: ServerArgs m deltaOut
-> deltaIn -> m () -- ^ invoked for each receive
}
hoistServerReturn :: (forall a. m a -> n a)
-> (forall a. n a -> m a)
-> ServerReturn m f initOut deltaIn deltaOut
-> ServerReturn n f initOut deltaIn deltaOut
hoistServerReturn f g ServerReturn{..} = ServerReturn
{ serverInitOut
, serverOnOpen = \args -> f $ serverOnOpen $ hoistServerArgs g args
, serverOnReceive = \args deltaIn -> f $ serverOnReceive (hoistServerArgs g args) deltaIn
}
data ServerContinue m f initOut deltaIn deltaOut = ServerContinue
{ serverContinue :: Broadcast m -> m (ServerReturn m f initOut deltaIn deltaOut)
, serverOnUnsubscribe :: m ()
}
hoistServerContinue :: Monad m
=> (forall a. m a -> n a)
-> (forall a. n a -> m a)
-> ServerContinue m f initOut deltaIn deltaOut
-> ServerContinue n f initOut deltaIn deltaOut
hoistServerContinue f g ServerContinue{..} = ServerContinue
{ serverContinue = \bcast -> f $ hoistServerReturn f g <$> serverContinue (hoistBroadcast g bcast)
, serverOnUnsubscribe = f serverOnUnsubscribe
}
type Server m f initIn initOut deltaIn deltaOut =
initIn -> m (Maybe (ServerContinue m f initOut deltaIn deltaOut))
hoistServer :: Monad m => Monad n
=> (forall a. m a -> n a)
-> (forall a. n a -> m a)
-> Server m f initIn initOut deltaIn deltaOut
-> Server n f initIn initOut deltaIn deltaOut
hoistServer f g server = \initIn -> do
mCont <- f $ server initIn
case mCont of
Nothing -> pure Nothing
Just cont -> pure $ Just $ hoistServerContinue f g cont
staticServer :: MonadIO m
=> Alternative f
=> (initIn -> m (Maybe initOut)) -- ^ Produce an initOut
-> Server m f initIn initOut JSONVoid JSONVoid
staticServer f initIn = do
mInitOut <- f initIn
case mInitOut of
Nothing -> pure Nothing
Just initOut -> pure $ Just ServerContinue
{ serverOnUnsubscribe = pure ()
, serverContinue = \_ -> pure ServerReturn
{ serverInitOut = initOut
, serverOnOpen = \ServerArgs{serverDeltaReject} -> do
serverDeltaReject
threadVar <- liftIO (newTVarIO empty)
pure threadVar
, serverOnReceive = \_ _ -> pure ()
}
}
-- ** Client
data ClientReturn m initOut deltaIn = ClientReturn
{ clientSendCurrent :: deltaIn -> m ()
, clientInitOut :: initOut
, clientUnsubscribe :: m ()
}
data ClientArgs m initIn initOut deltaIn deltaOut = ClientArgs
{ clientReceive :: ClientReturn m initOut deltaIn -> deltaOut -> m ()
, clientInitIn :: initIn
, clientOnReject :: m () -- ^ Run if the server decides to randomly kick the client
}
type Client m initIn initOut deltaIn deltaOut =
( ClientArgs m initIn initOut deltaIn deltaOut
-> m (Maybe (ClientReturn m initOut deltaIn))
) -> m ()
staticClient :: Monad m
=> ((initIn -> m (Maybe initOut)) -> m ()) -- ^ Obtain an initOut
-> Client m initIn initOut JSONVoid JSONVoid
staticClient f invoke = f $ \initIn -> do
mReturn <- invoke ClientArgs
{ clientInitIn = initIn
, clientReceive = \_ _ -> pure ()
, clientOnReject = pure ()
}
case mReturn of
Nothing -> pure Nothing
Just ClientReturn{clientInitOut,clientUnsubscribe} -> do
clientUnsubscribe
pure (Just clientInitOut)
-- ** Topic
newtype Topic = Topic {getTopic :: [Text]}
deriving (Eq, Ord, Generic, Hashable, NFData)
instance IsString Topic where
fromString x' =
let loop x = case T.breakOn "/" x of
(l,r)
| r == "" -> []
| otherwise -> l : loop (T.drop 1 r)
in Topic $ loop $ T.pack x'
instance Show Topic where
show (Topic x) = unpack (intercalate "/" x)
instance FromJSON Topic where
parseJSON = attoAeson (Topic <$> breaker)
where
breaker :: Parser [Text]
breaker = takeWhile1 (/= '/') `sepBy` char '/'
instance ToJSON Topic where
toJSON (Topic xs) = String (intercalate "/" xs)
-- ** Broadcast
type Broadcast m = Topic -> m (Maybe (Value -> Maybe (m ())))
hoistBroadcast :: Monad n => (forall a. m a -> n a) -> Broadcast m -> Broadcast n
hoistBroadcast f bcast = \topic -> do
mResolve <- f (bcast topic)
case mResolve of
Nothing -> pure Nothing
Just resolve -> pure $ Just $ \v -> f <$> resolve v
-- * JSON Encodings
data WithSessionID a = WithSessionID
{ withSessionIDSessionID :: {-# UNPACK #-} !SessionID
, withSessionIDContent :: a
} deriving (Eq, Show, Generic)
instance NFData a => NFData (WithSessionID a)
instance ToJSON a => ToJSON (WithSessionID a) where
toJSON WithSessionID{..} = object
[ "sessionID" .= withSessionIDSessionID
, "content" .= withSessionIDContent
]
instance FromJSON a => FromJSON (WithSessionID a) where
parseJSON (Object o) = WithSessionID <$> (o .: "sessionID") <*> (o .: "content")
parseJSON x = typeMismatch "WithSessionID" x
data WithTopic a = WithTopic
{ withTopicTopic :: !Topic
, withTopicContent :: a
} deriving (Eq, Show, Generic)
instance NFData a => NFData (WithTopic a)
instance ToJSON a => ToJSON (WithTopic a) where
toJSON WithTopic{..} = object
[ "topic" .= withTopicTopic
, "content" .= withTopicContent
]
instance FromJSON a => FromJSON (WithTopic a) where
parseJSON (Object o) = WithTopic <$> (o .: "topic") <*> (o .: "content")
parseJSON x = typeMismatch "WithTopic" x
data InitResponse a
= InitBadEncoding !LBS.ByteString
| InitDecodingError !String -- when manually decoding the content, casted
| InitRejected
| InitResponse a
deriving (Eq, Show, Generic)
instance NFData a => NFData (InitResponse a)
instance ToJSON a => ToJSON (InitResponse a) where
toJSON x = case x of
InitBadEncoding y -> object ["error" .= object ["badRequest" .= LT.decodeUtf8 y]]
InitDecodingError y -> object ["error" .= object ["decoding" .= y]]
InitRejected -> object ["error" .= String "rejected"]
InitResponse y -> object ["content" .= y]
instance FromJSON a => FromJSON (InitResponse a) where
parseJSON json = case json of
Object o -> do
let error' = do
json' <- o .: "error"
case json' of
String x
| x == "rejected" -> pure InitRejected
| otherwise -> fail'
Object o' -> do
let badEncoding = (InitBadEncoding . LT.encodeUtf8) <$> o' .: "badRequest"
decodingError = InitDecodingError <$> o' .: "decoding"
badEncoding <|> decodingError
_ -> fail'
response = InitResponse <$> o .: "content"
error' <|> response
_ -> fail'
where
fail' = typeMismatch "InitResponse" json
data WSHTTPResponse
= NoSessionID
deriving (Eq, Show, Generic)
instance NFData WSHTTPResponse
instance ToJSON WSHTTPResponse where
toJSON x = case x of
NoSessionID -> object ["error" .= String "no sessionID query parameter"]
instance FromJSON WSHTTPResponse where
parseJSON json = case json of
Object o -> do
json' <- o .: "error"
case json' of
String x
| x == "no sessionID query parameter" -> pure NoSessionID
| otherwise -> fail'
_ -> fail'
_ -> fail'
where
fail' = typeMismatch "WSHTTPResponse" json
data WSIncoming a
= WSUnsubscribe
{ wsUnsubscribeTopic :: !Topic
}
| WSIncoming a
deriving (Eq, Show, Generic)
instance NFData a => NFData (WSIncoming a)
instance ToJSON a => ToJSON (WSIncoming a) where
toJSON x = case x of
WSUnsubscribe topic -> object ["unsubscribe" .= topic]
WSIncoming y -> object ["content" .= y]
instance FromJSON a => FromJSON (WSIncoming a) where
parseJSON (Object o) = do
let unsubscribe = WSUnsubscribe <$> o .: "unsubscribe"
incoming = WSIncoming <$> o .: "content"
unsubscribe <|> incoming
parseJSON x = typeMismatch "WSIncoming" x
data WSOutgoing a
= WSTopicsSubscribed [Topic]
| WSTopicAdded !Topic
| WSTopicRemoved !Topic
| WSTopicRejected !Topic
| WSDecodingError !String
| WSOutgoing a
deriving (Eq, Show, Generic)
instance NFData a => NFData (WSOutgoing a)
instance ToJSON a => ToJSON (WSOutgoing a) where
toJSON x = case x of
WSDecodingError e -> object ["error" .= object ["decoding" .= e]]
WSTopicsSubscribed subs -> object ["subs" .= object ["init" .= subs]]
WSTopicAdded sub -> object ["subs" .= object ["add" .= sub]]
WSTopicRemoved sub -> object ["subs" .= object ["del" .= sub]]
WSTopicRejected sub -> object ["subs" .= object ["reject" .= sub]]
WSOutgoing y -> object ["content" .= y]
instance FromJSON a => FromJSON (WSOutgoing a) where
parseJSON json = case json of
Object o -> do
let content = WSOutgoing <$> o .: "content"
error' = do
json' <- o .: "error"
case json' of
Object o' -> do
let decodingError = WSDecodingError <$> o' .: "decoding"
decodingError
_ -> fail'
subs = do
json' <- o .: "subs"
case json' of
Object o' -> do
let subsInit = WSTopicsSubscribed <$> o' .: "init"
subAdd = WSTopicAdded <$> o' .: "add"
subDel = WSTopicRemoved <$> o' .: "del"
subReject = WSTopicRejected <$> o' .: "reject"
subsInit <|> subAdd <|> subDel <|> subReject
_ -> fail'
content <|> error' <|> subs
_ -> fail'
where
fail' = typeMismatch "WSOutgoing" json