hnix-store-remote-0.7.0.0: src/Data/Serializer/Example.hs
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Data.Serializer.Example
(
-- * Simple protocol
OpCode(..)
, Cmd(..)
-- * Cmd Serializer
, cmdS
-- * Runners
, runG
, runP
-- * Custom errors
, MyGetError(..)
, MyPutError(..)
-- ** Erroring variants of cmdS
-- *** putS with throwError and MyPutError
, cmdSPutError
-- *** getS with throwError and MyGetError
, cmdSGetError
-- *** getS with fail
, cmdSGetFail
-- *** putS with fail
, cmdSPutFail
-- * Elaborate
, cmdSRest
, runGRest
, runPRest
) where
import Control.Monad.Except (MonadError, throwError)
import Control.Monad.Reader (MonadReader, ask)
import Control.Monad.State (MonadState)
import Control.Monad.Trans (MonadTrans, lift)
import Control.Monad.Trans.Reader (ReaderT, runReaderT)
import Control.Monad.Trans.Except (ExceptT, runExceptT)
import Control.Monad.Trans.State (StateT, runStateT)
import Data.Bifunctor (first, second)
import Data.ByteString (ByteString)
import Data.Int (Int8)
import Data.GADT.Show (GShow(..), defaultGshowsPrec)
import Data.Kind (Type)
import Data.Type.Equality (TestEquality(..), (:~:)(Refl))
import Data.Serialize.Get (Get, getInt8)
import Data.Serialize.Put (Putter, PutM, putInt8)
import Data.Serializer
( Serializer(..)
, GetSerializerError
, runGetS
, runPutS
, transformGetError
, transformPutError
)
import Data.Some (Some(..))
import GHC.Generics (Generic)
import Test.QuickCheck (Arbitrary(..), oneof)
-- * Simple protocol
-- | OpCode used to differentiate between operations
data OpCode = OpCode_Int | OpCode_Bool
deriving (Bounded, Eq, Enum, Generic, Ord, Show)
-- | Protocol operations
data Cmd :: Type -> Type where
Cmd_Int :: Int8 -> Cmd Int8
Cmd_Bool :: Bool -> Cmd Bool
deriving instance Eq (Cmd a)
deriving instance Show (Cmd a)
instance GShow Cmd where
gshowsPrec = defaultGshowsPrec
instance TestEquality Cmd where
testEquality (Cmd_Int _) (Cmd_Int _) = Just Refl
testEquality (Cmd_Bool _) (Cmd_Bool _) = Just Refl
testEquality _ _ = Nothing
-- constructors only
-- import Data.GADT.Compare
-- instance GEq Cmd where
-- geq = testEquality
instance {-# OVERLAPPING #-} Eq (Some Cmd) where
Some (Cmd_Int a) == Some (Cmd_Int b) = a == b
Some (Cmd_Bool a) == Some (Cmd_Bool b) = a == b
_ == _ = False
instance Arbitrary (Some Cmd) where
arbitrary = oneof
[ Some . Cmd_Int <$> arbitrary
, Some . Cmd_Bool <$> arbitrary
]
-- | @OpCode@ @Serializer@
opcode :: MonadTrans t => Serializer t OpCode
opcode = Serializer
{ getS = lift getEnum
, putS = lift . putEnum
}
-- * Cmd Serializer
-- | @Cmd@ @Serializer@
cmdS
:: forall t . ( MonadTrans t
, Monad (t Get)
, Monad (t PutM)
)
=> Serializer t (Some Cmd)
cmdS = Serializer
{ getS = getS opcode >>= \case
OpCode_Int -> Some . Cmd_Int <$> lift getInt8
OpCode_Bool -> Some . Cmd_Bool <$> lift getBool
, putS = \case
Some (Cmd_Int i) -> putS opcode OpCode_Int >> lift (putInt8 i)
Some (Cmd_Bool b) -> putS opcode OpCode_Bool >> lift (putBool b)
}
-- * Runners
-- | @runGetS@ specialized to @ExceptT e@
runG
:: Serializer (ExceptT e) a
-> ByteString
-> Either (GetSerializerError e) a
runG s =
transformGetError
. runGetS s runExceptT
-- | @runPutS@ specialized to @ExceptT e@
runP
:: Serializer (ExceptT e) a
-> a
-> Either e ByteString
runP s =
(\(e, r) -> either Left (pure $ Right r) e)
. runPutS s runExceptT
-- * Custom errors
data MyGetError
= MyGetError_Example
deriving (Eq, Show)
data MyPutError
= MyPutError_NoLongerSupported -- no longer supported protocol version
deriving (Eq, Show)
-- ** Erroring variants of cmdS
-- *** putS with throwError and MyPutError
cmdSPutError :: Serializer (ExceptT MyPutError) (Some Cmd)
cmdSPutError = Serializer
{ getS = getS cmdS
, putS = \case
Some (Cmd_Int i) -> putS opcode OpCode_Int >> lift (putInt8 i)
Some (Cmd_Bool _b) -> throwError MyPutError_NoLongerSupported
}
-- *** getS with throwError and MyGetError
cmdSGetError :: Serializer (ExceptT MyGetError) (Some Cmd)
cmdSGetError = Serializer
{ getS = getS opcode >>= \case
OpCode_Int -> Some . Cmd_Int <$> lift getInt8
OpCode_Bool -> throwError MyGetError_Example
, putS = putS cmdS
}
-- *** getS with fail
cmdSGetFail
:: ( MonadTrans t
, MonadFail (t Get)
, Monad (t PutM)
)
=> Serializer t (Some Cmd)
cmdSGetFail = Serializer
{ getS = getS opcode >>= \case
OpCode_Int -> Some . Cmd_Int <$> lift getInt8
OpCode_Bool -> fail "no parse"
, putS = putS cmdS
}
-- *** putS with fail
-- | Unused as PutM doesn't have @MonadFail@
-- >>> serializerPutFail = cmdPutFail @(ExceptT MyGetError)
-- No instance for (MonadFail PutM)
-- as expected
cmdSPutFail
:: ( MonadTrans t
, MonadFail (t PutM)
, Monad (t Get)
)
=> Serializer t (Some Cmd)
cmdSPutFail = Serializer
{ getS = getS cmdS
, putS = \case
Some (Cmd_Int i) -> putS opcode OpCode_Int >> lift (putInt8 i)
Some (Cmd_Bool _b) -> fail "can't"
}
-- * Elaborate
-- | Transformer for @Serializer@
newtype REST r e s m a = REST
{ _unREST :: ExceptT e (StateT s (ReaderT r m)) a }
deriving
( Applicative
, Functor
, Monad
, MonadError e
, MonadReader r
, MonadState s
, MonadFail
)
instance MonadTrans (REST r e s) where
lift = REST . lift . lift . lift
-- | Runner for @REST@
restRunner
:: Monad m
=> r
-> s
-> REST r e s m a
-> m ((Either e a), s)
restRunner r s =
(`runReaderT` r)
. (`runStateT` s)
. runExceptT
. _unREST
runGRest
:: Serializer (REST r e s) a
-> r
-> s
-> ByteString
-> Either (GetSerializerError e) a
runGRest serializer r s =
transformGetError
. second fst
. runGetS
serializer
(restRunner r s)
runPRest
:: Serializer (REST r e s) a
-> r
-> s
-> a
-> Either e ByteString
runPRest serializer r s =
transformPutError
. first fst
. runPutS
serializer
(restRunner r s)
cmdSRest
:: Serializer (REST Bool e Int) (Some Cmd)
cmdSRest = Serializer
{ getS = getS opcode >>= \case
OpCode_Int -> do
isTrue <- ask
if isTrue
then Some . Cmd_Int . (+1) <$> lift getInt8
else Some . Cmd_Int <$> lift getInt8
OpCode_Bool -> Some . Cmd_Bool <$> lift getBool
, putS = \case
Some (Cmd_Int i) -> do
putS opcode OpCode_Int
isTrue <- ask
if isTrue
then lift (putInt8 (i - 1))
else lift (putInt8 i)
Some (Cmd_Bool b) -> putS opcode OpCode_Bool >> lift (putBool b)
}
-- Primitives helpers
getInt :: Integral a => Get a
getInt = fromIntegral <$> getInt8
putInt :: Integral a => Putter a
putInt = putInt8 . fromIntegral
-- | Deserialize @Bool@ from integer
getBool :: Get Bool
getBool = (getInt :: Get Int8) >>= \case
0 -> pure False
1 -> pure True
x -> fail $ "illegal bool value " ++ show x
-- | Serialize @Bool@ into integer
putBool :: Putter Bool
putBool True = putInt (1 :: Int8)
putBool False = putInt (0 :: Int8)
-- | Utility toEnum version checking bounds using Bounded class
toEnumCheckBounds
:: forall a
. ( Bounded a
, Enum a
)
=> Int
-> Either String a
toEnumCheckBounds = \case
x | x < fromEnum (minBound @a) -> Left $ "enum out of min bound " ++ show x
x | x > fromEnum (maxBound @a) -> Left $ "enum out of max bound " ++ show x
x | otherwise -> Right $ toEnum x
-- | Deserialize @Enum@ to integer
getEnum
:: ( Bounded a
, Enum a
)
=> Get a
getEnum =
toEnumCheckBounds <$> getInt
>>= either fail pure
-- | Serialize @Enum@ to integer
putEnum :: Enum a => Putter a
putEnum = putInt . fromEnum