packages feed

grpc-spec-1.0.0: src/Network/GRPC/Spec/Serialization/CustomMetadata.hs

{-# LANGUAGE OverloadedStrings #-}

module Network.GRPC.Spec.Serialization.CustomMetadata (
    -- * HeaderName
    buildHeaderName
  , parseHeaderName
    -- * AsciiValue
  , buildAsciiValue
  , parseAsciiValue
    -- * BinaryValue
  , buildBinaryValue
  , parseBinaryValue
    -- * CustomMetadata
  , buildCustomMetadata
  , parseCustomMetadata
  ) where

import Control.Monad
import Control.Monad.Except (MonadError(throwError))
import Data.ByteString qualified as BS.Strict
import Data.ByteString qualified as Strict (ByteString)
import Data.CaseInsensitive (CI)
import Data.CaseInsensitive qualified as CI
import Data.List (intersperse)
import Network.HTTP.Types qualified as HTTP

import Network.GRPC.Spec
import Network.GRPC.Spec.Serialization.Base64
import Network.GRPC.Spec.Util.ByteString (ascii)

{-------------------------------------------------------------------------------
  HeaderName
-------------------------------------------------------------------------------}

buildHeaderName :: HeaderName -> CI Strict.ByteString
buildHeaderName name =
    case name of
      BinaryHeader name' -> CI.mk name'
      AsciiHeader  name' -> CI.mk name'

parseHeaderName :: MonadError String m => CI Strict.ByteString -> m HeaderName
parseHeaderName name =
    case safeHeaderName (CI.foldedCase name) of
      Nothing    -> throwError $ "Invalid header name: " ++ show name
      Just name' -> return name'

{-------------------------------------------------------------------------------
  AsciiValue
-------------------------------------------------------------------------------}

buildAsciiValue :: Strict.ByteString -> Strict.ByteString
buildAsciiValue = id

parseAsciiValue ::
     MonadError String m
  => Strict.ByteString -> m Strict.ByteString
parseAsciiValue bs = do
    unless (isValidAsciiValue bs) $
      throwError $ "Invalid ASCII header: " ++ show bs
    return bs

{-------------------------------------------------------------------------------
  BinaryValue
-------------------------------------------------------------------------------}

-- | Serialize binary value (base-64 encoding)
buildBinaryValue :: Strict.ByteString -> Strict.ByteString
buildBinaryValue = encodeBase64

-- | Parse binary value
--
-- The presence of duplicate headers makes this a bit subtle. Let's consider an
-- example. Suppose we have two duplicate headers
--
-- > foo-bin: YWJj    -- encoding of "abc"
-- > foo-bin: ZGVm    -- encoding of "def"
--
-- The spec says
--
-- > Custom-Metadata header order is not guaranteed to be preserved except for
-- > values with duplicate header names. Duplicate header names may have their
-- > values joined with "," as the delimiter and be considered semantically
-- > equivalent.
--
-- We will do the decoding of both headers /prior/ to joining duplicate headers,
-- and so the value we will reconstruct for @foo-bin@ is \"abc,def\".
--
-- However, suppose we deal with a (non-compliant) peer which is unaware of
-- binary headers and has applied the joining rule /without/ decoding:
--
-- > foo-bin: YWJj,ZGVm
--
-- The spec is a bit vague about this case, saying only:
--
-- > Implementations must split Binary-Headers on "," before decoding the
-- > Base64-encoded values.
--
-- Here we assume that this case must be treated the same way as if the headers
-- /had/ been decoded prior to joining. Therefore, we split the input on commas,
-- decode each result separately, and join the results with commas again.
parseBinaryValue :: forall m.
     MonadError String m
  => Strict.ByteString -> m Strict.ByteString
parseBinaryValue bs = do
    let chunks = BS.Strict.split (ascii ',') bs
    decoded <- mapM decode chunks
    return $ mconcat $ intersperse "," decoded
  where
    decode :: Strict.ByteString -> m Strict.ByteString
    decode chunk =
        case decodeBase64 chunk of
          Left  err -> throwError err
          Right val -> return val

{-------------------------------------------------------------------------------
  CustomMetadata
-------------------------------------------------------------------------------}

-- | Serialize t'CustomMetadata'
buildCustomMetadata :: CustomMetadata -> HTTP.Header
buildCustomMetadata (CustomMetadata name value) =
    case name of
      BinaryHeader _ -> (buildHeaderName name, buildBinaryValue value)
      AsciiHeader  _ -> (buildHeaderName name, buildAsciiValue  value)

-- | Parse t'CustomMetadata'
parseCustomMetadata ::
     MonadError (InvalidHeaders GrpcException) m
  => HTTP.Header -> m CustomMetadata
parseCustomMetadata hdr@(name, value) = throwInvalidHeader hdr $ do
    name'     <- parseHeaderName name
    value'    <- case name' of
                   AsciiHeader  _ -> parseAsciiValue  value
                   BinaryHeader _ -> parseBinaryValue value
    -- If parsing succeeds, that justifies the use of 'UnsafeCustomMetadata'
    return $ CustomMetadata name' value'