{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} -------------------------------------------------------------------------------- -- | -- Module : EventSource.Types -- Copyright : (C) 2016 Yorick Laupa -- License : (see the file LICENSE) -- -- Maintainer : Yorick Laupa <yo.eight@gmail.com> -- Stability : provisional -- Portability : non-portable -- -------------------------------------------------------------------------------- module EventSource.Types where -------------------------------------------------------------------------------- import Control.Exception (Exception) import Control.Monad (MonadPlus, mzero) import Data.Bifunctor (first) import Data.Foldable (foldlM) import Data.Int (Int64) import Data.Semigroup (Semigroup) import Data.String (IsString(..)) import Data.String.Conversions (convertString) -------------------------------------------------------------------------------- import Control.Monad.Base (MonadBase, liftBase) import Control.Monad.State.Strict (State, modify, put) import Data.Aeson (ToJSON(..), FromJSON(..), Value, (.=), (.:)) import qualified Data.Aeson as Aeson import Data.Aeson.Types (Parser, parseEither) import Data.ByteString (ByteString) import qualified Data.HashMap.Strict as HashMap import qualified Data.Map.Strict as Map import Data.Text (Text) import Data.UUID (UUID, toText, fromText) import Data.UUID.V4 (nextRandom) -------------------------------------------------------------------------------- -- | Opaque data type used to store raw data. data Data = Data ByteString | DataAsJson Value -------------------------------------------------------------------------------- instance Show Data where show _ = "Data(*Binary data*)" -------------------------------------------------------------------------------- -- | Sometimes, having to implement a 'FromJSON' instance isn't flexible enough. -- 'JsonParsing' allow to pass parameters when parsing from a JSON value while -- remaining composable. newtype JsonParsing a = JsonParsing (Value -> Parser a) -------------------------------------------------------------------------------- instance Functor JsonParsing where fmap f (JsonParsing k) = JsonParsing $ \v -> f <$> k v -------------------------------------------------------------------------------- instance Applicative JsonParsing where pure a = JsonParsing $ \_ -> return a (JsonParsing kf) <*> (JsonParsing ka) = JsonParsing $ \v -> kf v <*> ka v -------------------------------------------------------------------------------- instance Monad JsonParsing where return = pure JsonParsing k >>= f = JsonParsing $ \v -> do a <- k v let JsonParsing km = f a km v -------------------------------------------------------------------------------- -- | Returns 'Data' content as a 'ByteString'. dataAsBytes :: Data -> ByteString dataAsBytes (Data bs) = bs dataAsBytes (DataAsJson v) = convertString $ Aeson.encode v -------------------------------------------------------------------------------- -- | Creates a 'Data' object from a raw 'ByteString'. dataFromBytes :: ByteString -> Data dataFromBytes = Data -------------------------------------------------------------------------------- -- | Creates a 'Data' object from a JSON object. dataFromJson :: ToJSON a => a -> Data dataFromJson = DataAsJson . toJSON -------------------------------------------------------------------------------- -- | Returns 'Data' content as any value that implements 'FromJSON' type-class. dataAsJson :: FromJSON a => Data -> Either Text a dataAsJson (Data bs) = first convertString $ Aeson.eitherDecodeStrict bs dataAsJson (DataAsJson v) = first convertString $ parseEither parseJSON v -------------------------------------------------------------------------------- -- | Uses a 'JsonParsing' comuputation to extract a value. dataAsParsing :: Data -> JsonParsing a -> Either Text a dataAsParsing dat (JsonParsing k) = do value <- dataAsJson dat first convertString $ parseEither k value -------------------------------------------------------------------------------- -- | Like 'dataAsParsing' but doesn't require you to use 'JsonParsing'. dataAsParse :: Data -> (Value -> Parser a) -> Either Text a dataAsParse dat k = dataAsParsing dat $ JsonParsing k -------------------------------------------------------------------------------- -- | Used to store a set a properties. One example is to be used as 'Event' -- metadata. newtype Properties = Properties (Map.Map Text Text) deriving (Semigroup, Monoid) -------------------------------------------------------------------------------- instance Show Properties where show (Properties m) = show m -------------------------------------------------------------------------------- instance ToJSON Properties where toJSON = Aeson.object . fmap go . properties where go (k, v) = k .= v -------------------------------------------------------------------------------- instance FromJSON Properties where parseJSON = Aeson.withObject "Properties" $ \o -> let go p k = fmap (\v -> setProperty k v p) (o .: k) in foldlM go mempty (HashMap.keys o) -------------------------------------------------------------------------------- -- | Retrieves a value associated with the given key. property :: MonadPlus m => Text -> Properties -> m Text property k (Properties m) = case Map.lookup k m of Nothing -> mzero Just v -> return v -------------------------------------------------------------------------------- -- | Builds a 'Properties' with a single pair of key-value. singleton :: Text -> Text -> Properties singleton k v = setProperty k v mempty -------------------------------------------------------------------------------- -- | Adds a pair of key-value into given 'Properties'. setProperty :: Text -> Text -> Properties -> Properties setProperty key value (Properties m) = Properties $ Map.insert key value m -------------------------------------------------------------------------------- -- | Returns all associated key-value pairs as a list. properties :: Properties -> [(Text, Text)] properties (Properties m) = Map.toList m -------------------------------------------------------------------------------- -- | Used to identify an event. newtype EventId = EventId UUID deriving (Eq, Ord) -------------------------------------------------------------------------------- instance ToJSON EventId where toJSON (EventId u) = toJSON (toText u) -------------------------------------------------------------------------------- instance FromJSON EventId where parseJSON = Aeson.withText "EventId" $ \t -> case fromText t of Just u -> return $ EventId u Nothing -> mzero -------------------------------------------------------------------------------- instance Show EventId where show (EventId uuid) = show uuid -------------------------------------------------------------------------------- -- | Generates a fresh 'EventId'. freshEventId :: MonadBase IO m => m EventId freshEventId = fmap EventId $ liftBase nextRandom -------------------------------------------------------------------------------- -- | Represents a stream name. newtype StreamName = StreamName Text deriving (Eq, Ord, ToJSON, FromJSON) -------------------------------------------------------------------------------- instance Show StreamName where show (StreamName s) = show s -------------------------------------------------------------------------------- instance IsString StreamName where fromString = StreamName . fromString -------------------------------------------------------------------------------- -- | Used to identity the type of an 'Event'. newtype EventType = EventType Text deriving (Eq, ToJSON, FromJSON) -------------------------------------------------------------------------------- instance Show EventType where show (EventType t) = show t -------------------------------------------------------------------------------- instance IsString EventType where fromString = EventType . fromString -------------------------------------------------------------------------------- -- | Sets 'EventType' for an 'Event'. setEventType :: EventType -> State Event () setEventType typ = modify $ \s -> s { eventType = typ } -------------------------------------------------------------------------------- -- | Sets 'Eventid' for an 'Event'. setEventId :: EventId -> State Event () setEventId eid = modify $ \s -> s { eventId = eid } -------------------------------------------------------------------------------- -- | Sets a payload for an 'Event'. setEventPayload :: Data -> State Event () setEventPayload dat = modify $ \s -> s { eventPayload = dat } -------------------------------------------------------------------------------- -- | Sets metadata for an 'Event'. setEventMetadata :: Properties -> State Event () setEventMetadata props = modify $ \s -> s { eventMetadata = Just props } -------------------------------------------------------------------------------- -- | Encapsulates an event which is about to be saved. data Event = Event { eventType :: !EventType , eventId :: !EventId , eventPayload :: !Data , eventMetadata :: !(Maybe Properties) } deriving Show -------------------------------------------------------------------------------- -- | Represents an event index in a stream. newtype EventNumber = EventNumber Int64 deriving (Eq, Ord, Num, Enum, Show, FromJSON, ToJSON) -------------------------------------------------------------------------------- -- | Represents an event that's saved into the event store. data SavedEvent = SavedEvent { eventNumber :: !EventNumber , savedEvent :: !Event , linkEvent :: !(Maybe Event) } deriving Show -------------------------------------------------------------------------------- -- | Deserializes a 'SavedEvent'. savedEventAs :: DecodeEvent a => SavedEvent -> Either Text a savedEventAs = decodeEvent . savedEvent -------------------------------------------------------------------------------- -- | Represents batch of events read from a store. data Slice' a = Slice' { sliceEvents :: ![SavedEvent] , sliceEndOfStream :: !Bool , sliceNext :: !a } deriving Show -------------------------------------------------------------------------------- type Slice = Slice' EventNumber -------------------------------------------------------------------------------- sliceNextEventNumber :: Slice -> EventNumber sliceNextEventNumber = sliceNext -------------------------------------------------------------------------------- -- | Deserializes a 'Slice''s events. sliceEventsAs :: DecodeEvent a => Slice -> Either Text [a] sliceEventsAs = traverse savedEventAs . sliceEvents -------------------------------------------------------------------------------- -- | Encodes a data object into an 'Event'. 'encodeEvent' get passed an -- 'EventId' in a case where a fresh id is needed. class EncodeEvent a where encodeEvent :: a -> State Event () -------------------------------------------------------------------------------- -- | Decodes an 'Event' into a data object. class DecodeEvent a where decodeEvent :: Event -> Either Text a -------------------------------------------------------------------------------- newtype DecodeEventException = DecodeEventException Text deriving Show -------------------------------------------------------------------------------- instance Exception DecodeEventException -------------------------------------------------------------------------------- instance EncodeEvent Event where encodeEvent e = put e -------------------------------------------------------------------------------- instance DecodeEvent Event where decodeEvent = Right -------------------------------------------------------------------------------- -- | The purpose of 'ExpectedVersion' is to make sure a certain stream state is -- at an expected point in order to carry out a write. data ExpectedVersion = AnyVersion -- Stream is a any given state. | NoStream -- Stream shouldn't exist. | StreamExists -- Stream should exist. | ExactVersion EventNumber -- Stream should be at givent event number. deriving (Show, Eq) -------------------------------------------------------------------------------- -- | Statuses you can get on every read attempt. data ReadStatus a = ReadSuccess a | ReadFailure ReadFailure deriving Show -------------------------------------------------------------------------------- -- | Returns 'True' is 'ReadStatus' is a 'ReadSuccess'. isReadSuccess :: ReadStatus a -> Bool isReadSuccess (ReadSuccess _) = True isReadSuccess _ = False -------------------------------------------------------------------------------- -- | Returns 'False' is 'ReadStatus' is a 'ReadFailure'. isReadFailure :: ReadStatus a -> Bool isReadFailure (ReadFailure _) = True isReadFailure _ = False -------------------------------------------------------------------------------- -- | Represents the different kind of failure you can get when reading. data ReadFailure = StreamNotFound StreamName | ReadError (Maybe Text) | AccessDenied StreamName deriving Show -------------------------------------------------------------------------------- instance Exception ReadFailure -------------------------------------------------------------------------------- instance Functor ReadStatus where fmap f (ReadSuccess a) = ReadSuccess $ f a fmap _ (ReadFailure e) = ReadFailure e -------------------------------------------------------------------------------- instance Foldable ReadStatus where foldMap f (ReadSuccess a) = f a foldMap _ _ = mempty -------------------------------------------------------------------------------- instance Traversable ReadStatus where traverse f (ReadSuccess a) = fmap ReadSuccess $ f a traverse _ (ReadFailure e) = pure $ ReadFailure e