packages feed

grapesy-1.0.0: src/Network/GRPC/Common/Compression.hs

-- | Public 'Compression' API
--
-- Intended for qualified import.
--
-- > import Network.GRPC.Common.Compression (Compression(..))
-- > import Network.GRPC.Common.Compression qualified as Compr
module Network.GRPC.Common.Compression (
    -- * Definition
    Compression(..)
  , CompressionId(..)
    -- * Standard compression schemes
  , gzip
  , allSupportedCompression
    -- * Negotation
  , Negotation(..)
  , getSupported
    -- ** Specific negotation strategies
  , none
  , chooseFirst
  , only
  , insist
  ) where

import Data.Default
import Data.Foldable (toList)
import Data.List.NonEmpty (NonEmpty(..))
import Data.List.NonEmpty qualified as NE
import Data.Map (Map)
import Data.Map qualified as Map

import Network.GRPC.Spec

{-------------------------------------------------------------------------------
  Negotation
-------------------------------------------------------------------------------}

-- | Compression negotation
data Negotation = Negotation {
      -- | Which algorithms should be offered (in this order) to the peer?
      --
      -- This should normally always include 'Identity' (see 'choose');
      -- but see 'insist'.
      offer :: NonEmpty CompressionId

      -- | Choose compression algorithm
      --
      -- We will run this only once per open connection, when the server first
      -- tells us their list of supported compression algorithms. Unless
      -- compression negotation has taken place, no compression should be used.
      --
      -- This cannot fail: the least common denominator is to use no
      -- compression. This must be allowed, because the gRPC specification
      -- /anyway/ allows a per-message flag to indicate whether the message
      -- is compressed or not; thus, even if a specific compression algorithm
      -- is negotiated, there is no guarantee anything is compressed.
    , choose :: NonEmpty CompressionId -> Compression

      -- | All supported compression algorithms
    , supported :: Map CompressionId Compression
    }

-- | Map 'CompressionId' to 'Compression' for supported algorithms
getSupported :: Negotation -> CompressionId -> Maybe Compression
getSupported compr cid = Map.lookup cid (supported compr)

instance Default Negotation where
  def = chooseFirst allSupportedCompression

-- | Disable all compression
none :: Negotation
none = insist noCompression

-- | Choose the first algorithm that appears in the list of peer supported
--
-- Precondition: the list should include the identity.
chooseFirst :: NonEmpty Compression -> Negotation
chooseFirst ourSupported = Negotation {
      offer =
        fmap compressionId ourSupported
    , choose = \peerSupported ->
        let peerSupports :: Compression -> Bool
            peerSupports = (`elem` peerSupported) . compressionId
        in case NE.filter peerSupports ourSupported of
             c:_ -> c
             []  -> noCompression
    , supported =
        Map.fromList $
          map (\c -> (compressionId c, c)) (toList ourSupported)
    }

-- | Only use the given algorithm, if the peer supports it
only :: Compression -> Negotation
only compr = chooseFirst (compr :| [noCompression])

-- | Insist on the specified algorithm, /no matter what the peer offers/
--
-- This is dangerous: if the peer does not support the specified algorithm, it
-- will be unable to decompress any messages. Primarily used for testing.
--
-- See also 'only'.
insist :: Compression -> Negotation
insist compr = Negotation {
      offer     = compressionId compr :| []
    , choose    = \_ -> compr
    , supported = Map.singleton (compressionId compr) compr
    }