typed-protocols-1.2.0.0: src/Network/TypedProtocol/Codec.hs
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE QuantifiedConstraints #-}
-- @UndecidableInstances@ extension is required for defining @Show@ instance of
-- @'AnyMessage'@ and @'AnyMessage'@.
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE ViewPatterns #-}
module Network.TypedProtocol.Codec
( -- * Defining and using Codecs
-- ** Codec type
CodecF (..)
, Codec
, Annotator (..)
, AnnotatedCodec
, hoistAnnotation
, unAnnotateCodec
, hoistCodec
, isoCodec
, mapFailureCodec
-- ** Incremental decoding
, DecodeStep (..)
, runDecoder
, runDecoderPure
, hoistDecodeStep
, isoDecodeStep
, mapFailureDecodeStep
-- ** Related types
-- *** SomeMessage
, SomeMessage (..)
-- *** StateToken
, StateToken
, StateTokenI (..)
-- *** ActiveState
, IsActiveState (..)
, ActiveState
, ActiveAgency
, ActiveAgency' (..)
, notActiveState
-- *** PeerRole
, PeerRole (..)
-- * CodecFailure
, CodecFailure (..)
-- * Testing codec properties
-- ** Codec
, AnyMessage (AnyMessage, AnyMessageAndAgency)
-- ** SomeState
, SomeState (..)
) where
import Control.DeepSeq (NFData (..))
import Control.Exception (Exception)
import Data.Kind (Type)
import Network.TypedProtocol.Core
import Network.TypedProtocol.Driver (SomeMessage (..))
-- | A codec for a 'Protocol' handles the encoding and decoding of typed
-- protocol messages. This is typically used when sending protocol messages
-- over untyped channels. The codec chooses the exact encoding, for example
-- encoding in some text-based syntax, or some choice of binary format.
--
-- The codec is parametrised by:
--
-- * The protocol
-- * the type of decoding failures
-- * the monad in which the decoder runs
-- * a functor which wraps the decoder result, e.g. `SomeMessage` or `Annotator`.
-- * the type of the encoded data (typically strings or bytes, or
-- `AnyMessage` for testing purposes with no codec overhead).
--
-- A codec consists of a message encoder and a decoder.
--
-- The `CodecF` type comes with two useful type aliases:
-- * `Codec` - which can decode protocol messages
-- * `AnnotatedCodec` - which also has access to bytes which were fed to the
-- codec when decoding a message.
--
-- `AnnotatedCodec` is useful if one wants to decode data structures and retain
-- their CBOR encoding (`decodeWithByteSpan` from `cborg` can be used for that
-- purpose).
--
-- The encoder is supplied both with the message to encode and the current
-- protocol state (matching the message). The protocol state can be either
-- a client or server state, but for either peer role it is a protocol state
-- in which the peer has agency, since those are the only states where a
-- peer needs to encode a message to be able to send it.
--
-- For example a simple text encoder for the ping\/pong protocol could be:
--
-- > encode :: SingI st
-- > => Message PingPong st st'
-- > -> String
-- > encode MsgPing = "ping\n"
-- > encode MsgDone = "done\n"
-- > encode MsgPong = "pong\n"
--
-- The decoder is also given the current protocol state and it is expected to
-- be able to decode /any/ message that is valid in that state, but /only/
-- messages that are valid in that state. Messages that are unexpected for the
-- current state should be treated like any other decoding format error.
--
-- While the current protocol state is known, the state that the message will
-- have the peer transition to is not known. For this reason the decoded
-- message is wrapped in the 'SomeMessage' constructor which hides the \"to\"
-- state.
--
-- The decoder uses an incremental decoding interface 'DecodeStep' so that
-- input can be supplied (e.g. from a Channel) bit by bit. This style of
-- decoder allows but does not require a format with message framing where the
-- decoder input matches exactly with the message boundaries.
--
-- > decode :: forall st m. SingI st
-- > => StateToken st
-- > -> m (DecodeStep String String m (SomeMessage st))
-- > decode stok =
-- > decodeTerminatedFrame '\n' $ \str trailing ->
-- > case (stok, str) of
-- > (SingBusy, "pong") ->
-- > DecodeDone (SomeMessage MsgPong) trailing
-- > (SingIdle, "ping") ->
-- > DecodeDone (SomeMessage MsgPing) trailing
-- > (SingIdle, "done") ->
-- > DecodeDone (SomeMessage MsgDone) trailing
-- > _ -> DecodeFail ("unexpected message: " ++ str)
--
-- See "typed-protocols-examples" for the full example.
--
-- Note that the pattern matching on the combination of the message string and
-- the protocol state. This neatly fulfils the requirement that we only return
-- messages that are of the correct type for the given protocol state.
--
-- This toy example format uses newlines @\n@ as a framing format. See
-- 'DecodeStep' for suggestions on how to use it for more realistic formats.
--
data CodecF ps failure m (f :: ps -> Type) bytes = Codec {
encode :: forall (st :: ps) (st' :: ps).
StateTokenI st
=> ActiveState st
=> Message ps st st'
-> bytes,
-- ^ encode a message into `bytes`
decode :: forall (st :: ps).
ActiveState st
=> StateToken st
-- evidence for an active state
-> m (DecodeStep bytes failure m (f st))
-- ^ decode a message knowing that the current state is `st`
}
-- | Change functor in which the codec is running.
--
-- | The type of a standard `Codec` for `typed-protocols`.
--
type Codec ps failure m bytes = CodecF ps failure m SomeMessage bytes
-- | A continuation for a decoder which is fed with whole bytes that were used
-- to parse the message.
--
newtype Annotator bytes st = Annotator { runAnnotator :: bytes -> SomeMessage st }
-- | Codec which has access to bytes received from the network to annotate
-- decoded structure.
--
-- AnnotatedCodec works in two stages. First it is decoding the structure as
-- bytes are received from the network, like a `Codec` does. The codec returns
-- a continuation `Annotator` which is fed with all bytes used to parse the
-- message. It is the driver which is responsible for passing bytes which were
-- fed to the incremental codec.
--
type AnnotatedCodec ps failure m bytes = CodecF ps failure m (Annotator bytes) bytes
-- | Transform annotation.
--
hoistAnnotation :: forall ps failure m f g bytes.
Functor m
=> (forall st. f st -> g st)
-> CodecF ps failure m f bytes
-> CodecF ps failure m g bytes
hoistAnnotation nat codec@Codec { decode } = codec { decode = decode' }
where
decode' :: forall (st :: ps).
ActiveState st
=> StateToken st
-> m (DecodeStep bytes failure m (g st))
decode' tok = fmap nat <$> decode tok
-- | Remove annotation. It is only safe if the `Annotator` treats empty input
-- in a safe way.
--
unAnnotateCodec :: forall ps failure m bytes.
(Functor m, Monoid bytes)
=> AnnotatedCodec ps failure m bytes
-> Codec ps failure m bytes
unAnnotateCodec = hoistAnnotation (($ mempty) . runAnnotator)
hoistCodec
:: ( Functor n )
=> (forall x . m x -> n x)
-- ^ a natural transformation
-> CodecF ps failure m f bytes
-> CodecF ps failure n f bytes
hoistCodec nat codec = codec
{ decode = fmap (hoistDecodeStep nat) . nat . decode codec
}
-- | Change bytes of a codec.
--
isoCodec :: Functor m
=> (bytes -> bytes')
-- ^ map from 'bytes' to `bytes'`
-> (bytes' -> bytes)
-- ^ its inverse
-> CodecF ps failure m f bytes
-- ^ codec
-> CodecF ps failure m f bytes'
isoCodec f finv Codec {encode, decode} = Codec {
encode = \msg -> f $ encode msg,
decode = \tok -> isoDecodeStep f finv <$> decode tok
}
-- | Modify failure type.
--
mapFailureCodec
:: Functor m
=> (failure -> failure')
-- ^ a function to apply to failure
-> CodecF ps failure m f bytes
-> CodecF ps failure' m f bytes
mapFailureCodec f Codec {encode, decode} = Codec {
encode = encode,
decode = \tok -> mapFailureDecodeStep f <$> decode tok
}
-- | An incremental decoder with return a value of type @a@.
--
-- This interface is not designed to be used directly for implementing
-- decoders, only for running them. In real applications it is expected to use
-- libraries for text or binary decoding and to implement appropriate wrappers
-- to match up with this incremental decoder interface.
--
-- This style of interface already closely matches that provided by libraries
-- such as @attoparsec@ for text formats, and @binary@, @cereal@ and @cborg@
-- for binary formats.
--
data DecodeStep bytes failure m a =
-- | The decoder has consumed the available input and needs more
-- to continue. Provide @'Just'@ if more input is available and
-- @'Nothing'@ otherwise, and you will get a new @'DecodeStep'@.
DecodePartial (Maybe bytes -> m (DecodeStep bytes failure m a))
-- | The decoder has successfully finished. This provides the decoded
-- result value plus any unused input.
| DecodeDone a (Maybe bytes)
-- | The decoder ran into an error. The decoder either used
-- @'fail'@ or was not provided enough input.
| DecodeFail failure
deriving instance Functor m => Functor (DecodeStep bytes failure m)
-- | Change bytes of 'DecodeStep'.
--
isoDecodeStep
:: Functor m
=> (bytes -> bytes')
-- ^ map from 'bytes' to `bytes'`
-> (bytes' -> bytes)
-- its inverse
-> DecodeStep bytes failure m a
-> DecodeStep bytes' failure m a
isoDecodeStep f finv (DecodePartial g) = DecodePartial (fmap (isoDecodeStep f finv) . g . fmap finv)
isoDecodeStep f _finv (DecodeDone a bytes) = DecodeDone a (fmap f bytes)
isoDecodeStep _f _finv (DecodeFail failure) = DecodeFail failure
-- | Change functor in which the codec is running.
--
hoistDecodeStep
:: ( Functor n )
=> (forall x . m x -> n x)
-- ^ a natural transformation
-> DecodeStep bytes failure m a
-> DecodeStep bytes failure n a
hoistDecodeStep nat step = case step of
DecodeDone a mb -> DecodeDone a mb
DecodeFail fail_AvoidNameShadow -> DecodeFail fail_AvoidNameShadow
DecodePartial k -> DecodePartial (fmap (hoistDecodeStep nat) . nat . k)
-- | Modify failure type.
--
mapFailureDecodeStep
:: Functor m
=> (failure -> failure')
-- ^ a function to apply to failure
-> DecodeStep bytes failure m a
-> DecodeStep bytes failure' m a
mapFailureDecodeStep f step = case step of
DecodeDone a mb -> DecodeDone a mb
DecodeFail failure -> DecodeFail (f failure)
DecodePartial k -> DecodePartial (fmap (mapFailureDecodeStep f) . k)
-- | Each 'Codec' can use whatever @failure@ type is appropriate. This simple
-- exception type is provided for use by simple codecs (e.g. \"identity\") when
-- nothing more than a 'String' is needed. It is an instance of 'Exception'.
--
data CodecFailure = CodecFailureOutOfInput
| CodecFailure String
deriving (Eq, Show)
instance NFData CodecFailure where
rnf CodecFailureOutOfInput = ()
rnf (CodecFailure failure) = rnf failure
-- safe instance with @UndecidableInstances@ in scope
instance Exception CodecFailure
--
-- Running decoders
--
-- | Run a codec incremental decoder 'DecodeStep' against a list of input.
--
-- It ignores any unused trailing data. This is useful for demos, quick
-- experiments and tests.
--
-- See also 'Network.TypedProtocol.Driver.runDecoderWithChannel'
--
runDecoder :: Monad m
=> [bytes]
-- ^ bytes to be fed into the incremental 'DecodeStep'
-> DecodeStep bytes failure m a
-- ^ decoder
-> m (Either failure a)
runDecoder _ (DecodeDone x _trailing) = return (Right x)
runDecoder _ (DecodeFail failure) = return (Left failure)
runDecoder [] (DecodePartial k) = k Nothing >>= runDecoder []
runDecoder (b:bs) (DecodePartial k) = k (Just b) >>= runDecoder bs
-- | A variant of 'runDecoder' that is suitable for \"pure\" monads that have
-- a run function. This includes 'ST', using 'Control.Monad.ST.runST'.
--
runDecoderPure :: Monad m
=> (forall b. m b -> b)
-- ^ run monad 'm' in a pure way, e.g. 'runIdentity'
-> m (DecodeStep bytes failure m a)
-> [bytes]
-- ^ input bytes
-> Either failure a
runDecoderPure runM decoder bs = runM (runDecoder bs =<< decoder)
--
-- Codec properties
--
-- | Any message for a protocol, with a 'StateTokenI' constraint which gives access to
-- protocol state.
--
-- Used where we don't know statically what the state type is, but need the
-- agency and message to match each other.
--
data AnyMessage ps where
AnyMessage :: forall ps (st :: ps) (st' :: ps).
( StateTokenI st
, ActiveState st
)
=> Message ps (st :: ps) (st' :: ps)
-- ^ 'Message' between some states
-> AnyMessage ps
-- requires @UndecidableInstances@ and @QuantifiedConstraints@.
instance (forall (st :: ps) (st' :: ps). Show (Message ps st st'))
=> Show (AnyMessage ps) where
showsPrec d (AnyMessage (msg :: Message ps st st')) =
showParen (d > app_prec) (showString "AnyMessage " . showsPrec (app_prec + 1) msg)
where
app_prec = 10
-- | A convenient pattern synonym which unwrap 'AnyMessage' giving both the
-- singleton for the state and the message.
--
pattern AnyMessageAndAgency :: forall ps. ()
=> forall (st :: ps) (st' :: ps).
(StateTokenI st, ActiveState st)
=> StateToken st
-> Message ps st st'
-> AnyMessage ps
pattern AnyMessageAndAgency stateToken msg <- AnyMessage (getAgency -> (msg, stateToken))
where
AnyMessageAndAgency _ msg = AnyMessage msg
{-# COMPLETE AnyMessageAndAgency #-}
-- | Internal view pattern for 'AnyMessageAndAgency'
--
getAgency :: StateTokenI st => Message ps st st' -> (Message ps st st', StateToken st)
getAgency msg = (msg, stateToken)
-- | Auxiliary definition for 'prop_codec_binary_compatM'.
--
-- Used for the existential @st :: ps@ parameter when expressing that for each
-- value of 'PeerHasAgency' for protocol A, there is a corresponding
-- 'PeerHasAgency' for protocol B of some @st :: ps@.
data SomeState (ps :: Type) where
SomeState
:: forall ps (st :: ps).
ActiveState st
=> StateToken st
-- ^ state token for some active state 'st'
-> SomeState ps