packages feed

waypoint-0.1: lib/Waypoint/Header.hs

{-# LANGUAGE StrictData #-}
{-# LANGUAGE NoFieldSelectors #-}
{-# OPTIONS_HADDOCK not-home #-}

-- | Usually, you don't need to import this module unless
-- you are trying to reuse some of 'HeaderCodec'\'s underlying primitives.
-- Just import "Waypoint" instead.
module Waypoint.Header
   ( -- * HeaderValue
    HeaderValue (..)
   , ToHeaderValue (..)
   , FromHeaderValue (..)

    -- * HeaderValues
   , HeaderValues
   , headerValuesOne
   , headerValuesMaybe
   , headerValuesMany
   , headerValuesSome

    -- * HeaderF
   , HeaderF (..)
   , headerFDecode
   , HeaderFDecodeState(..)
   , headerFEncode
   , headerFNames

    -- * Header
   , HeaderCodec (..)
   , headerDecode
   , headerEncode
   , headerNames
   , header
   , headerMaybe
   , headerSome
   , headerMany

    -- * ErrHeader
   , ErrHeader (..)
   )
where

import Control.Applicative
import Control.Applicative.Free.Fast
import Control.Exception (Exception)
import Control.Monad
import Control.Monad.Trans.State.Strict
import Data.ByteString qualified as B
import Data.List qualified as List
import Data.List.NonEmpty qualified as NEL
import Data.Monoid (Endo (..))
import Data.Profunctor
import Data.Set qualified as Set
import Data.Text qualified as T
import Data.Text.Encoding qualified as T
import Data.Tuple
import Network.HTTP.Types qualified as H
import Text.Read (readMaybe)
import Witherable qualified as W
import Prelude

--------------------------------------------------------------------------------

-- | How to encode and decode a single header value
-- (e.g., the @v@ in @k: v@).
data HeaderValue i o = HeaderValue
   { encode :: i -> B.ByteString
   -- ^ __WARNING__: The 'B.ByteString' is literally as it is on the header
   -- value.
   , decode :: B.ByteString -> Maybe o
   -- ^ __WARNING__: The 'B.ByteString' is literally as it is on the header
   -- value.
   }

instance W.Filterable (HeaderValue i) where
   mapMaybe f (HeaderValue i o) = HeaderValue i (o >=> f)

instance Functor (HeaderValue i) where
   fmap = rmap

instance Profunctor HeaderValue where
   dimap f g (HeaderValue i o) = HeaderValue (i . f) (fmap g . o)

-- | 'HeaderValue' based on 'ToHeaderValue' and 'FromHeaderValue'.
headerValue :: (ToHeaderValue i, FromHeaderValue o) => HeaderValue i o
headerValue = HeaderValue toHeaderValue fromHeaderValue

--------------------------------------------------------------------------------

-- | How to encode and decode potentially many header line values
-- (e.g., both @v0@ and @v1@ in multiple lines @k: v0@ and @k: v1@).
data HeaderValues i o = HeaderValues
   { encode :: i -> [B.ByteString]
   , decode :: [B.ByteString] -> Maybe o
   }

instance Functor (HeaderValues i) where
   fmap = rmap

instance Profunctor HeaderValues where
   dimap f g (HeaderValues i o) = HeaderValues (i . f) (fmap g . o)

instance W.Filterable (HeaderValues i) where
   mapMaybe f (HeaderValues i o) = HeaderValues i (o >=> f)

headerValuesOne :: HeaderValue i o -> HeaderValues i o
headerValuesOne v = HeaderValues (pure . v.encode) \case
   [t] -> v.decode t
   _ -> Nothing

headerValuesMaybe :: HeaderValue i o -> HeaderValues (Maybe i) (Maybe o)
headerValuesMaybe v = HeaderValues (maybe [] (pure . v.encode)) \case
   [] -> Just Nothing
   [t] -> Just <$> v.decode t
   _ -> Nothing

headerValuesSome
   :: HeaderValue i o -> HeaderValues (NEL.NonEmpty i) (NEL.NonEmpty o)
headerValuesSome v = HeaderValues (fmap v.encode . NEL.toList) \case
   t : ts -> traverse v.decode (t NEL.:| ts)
   _ -> Nothing

headerValuesMany :: HeaderValue i o -> HeaderValues [i] [o]
headerValuesMany v = HeaderValues (fmap v.encode) (traverse v.decode)

--------------------------------------------------------------------------------

-- | Like 'HeaderValues', but with the added @name@.
data HeaderF i o = HeaderF
   { name :: H.HeaderName
   , values :: HeaderValues i o
   }

instance Functor (HeaderF i) where
   fmap = rmap

instance Profunctor HeaderF where
   dimap f g (HeaderF k v) = HeaderF k (dimap f g v)

instance W.Filterable (HeaderF i) where
   mapMaybe f (HeaderF k v) = HeaderF k (W.mapMaybe f v)

--------------------------------------------------------------------------------

-- | See 'headerFDecode'.
data HeaderFDecodeState = HeaderFDecodeState
   { seen :: Set.Set H.HeaderName
   -- ^ Previously seen header names.
   , input :: [H.Header]
   -- ^ Available raw input to decode.
   }
   deriving (Eq, Show)

-- | You probably don't need to use this unless you are building an
-- 'Applicative' on top of 'HeaderF' yourself.
headerFDecode
   :: HeaderF i o
   -> HeaderFDecodeState
   -> Either ErrHeader (o, HeaderFDecodeState)
headerFDecode q = \s ->
   if Set.member q.name s.seen
      then Left $ ErrHeaderBadDecoder q.name
      else do
         let (xs1, xs2) = List.partition (\(k, _) -> k == q.name) s.input
         case q.values.decode (fmap snd xs1) of
            Just a -> Right (a, s{seen = Set.insert q.name s.seen, input = xs2})
            Nothing -> Left $ ErrHeaderDecode q.name

-- | You probably don't need to use this unless you are building an
-- 'Applicative' on top of 'HeaderF' yourself.
headerFEncode :: HeaderF i o -> i -> Endo [H.Header]
headerFEncode q = \i ->
   foldMap
      (\v -> Endo ((q.name, v) :))
      (q.values.encode i)
      <> Endo (filter (\(k, _) -> k /= q.name))

-- | You probably don't need to use this unless you are building an
-- 'Applicative' on top of 'HeaderF' yourself.
headerFNames :: HeaderF i o -> Endo [H.HeaderName]
headerFNames q = Endo ((q.name :) . filter (/= q.name))

--------------------------------------------------------------------------------

data ErrHeader
   = -- | Error decoding the header value at the given name.
     ErrHeaderDecode H.HeaderName
   | -- | This error means the decoder itself is incorrect. It tried to
     -- decode values associated to the same header name more than once.
     ErrHeaderBadDecoder H.HeaderName
   deriving stock (Eq, Show)
   deriving anyclass (Exception)

--------------------------------------------------------------------------------

-- | Bidirectional codec for encoding an @i@ into HTTP headers, and decoding
-- HTTP headers into an @o@.
--
-- Use 'header', 'headerMaybe', 'headerSome', 'headerMany' and 'Applicative' to
-- construct.
newtype HeaderCodec i o = HeaderCodec (Ap (HeaderF i) o)
   deriving newtype (Functor, Applicative)

instance Profunctor HeaderCodec where
   rmap = fmap
   lmap f (HeaderCodec a) = HeaderCodec $ hoistAp (lmap f) a

-- | Consumes as much of the 'H.Header's as necessary in order to produce @o@.
-- Returns any leftovers in the original order.
headerDecode :: HeaderCodec i o -> [H.Header] -> Either ErrHeader (o, [H.Header])
headerDecode (HeaderCodec af) = \xs0 -> do
   let s0 = HeaderFDecodeState{seen = mempty, input = xs0}
   (o, s1) <- runStateT (runAp (StateT . headerFDecode) af) s0
   pure (o, s1.input)

headerEncode :: HeaderCodec i o -> i -> [H.Header]
headerEncode (HeaderCodec af) = flip appEndo [] . runAp_ headerFEncode af

-- | Names in the order they are encoded and decoded.
headerNames :: HeaderCodec i o -> [H.HeaderName]
headerNames (HeaderCodec af) = appEndo (runAp_ headerFNames af) []

--------------------------------------------------------------------------------

-- | Encode and decode the one the header value for a particular name.
--
-- E.g., @__name__: __value__@.
header
   :: (ToHeaderValue x, FromHeaderValue o)
   => H.HeaderName
   -- ^ Header name.
   -> (i -> x)
   -- ^ @'header' k f == 'lmap' f ('header' k 'id')@, provided just for
   -- convenience.
   -> HeaderCodec i o
header k f =
   HeaderCodec $ liftAp $ HeaderF k $ lmap f $ headerValuesOne headerValue

-- | Encode and decode zero or one header string values for a particular name.
--
-- E.g., @__name__: __value__@, or just no mention of @__name__@ altogether.
headerMaybe
   :: (ToHeaderValue x, FromHeaderValue o)
   => H.HeaderName
   -- ^ Header name.
   -> (i -> Maybe x)
   -- ^ @'headerMaybe' k f == 'lmap' f ('headerMaybe' k 'id')@, provided just for
   -- convenience.
   -> HeaderCodec i (Maybe o)
headerMaybe k f =
   HeaderCodec $ liftAp $ HeaderF k $ lmap f $ headerValuesMaybe headerValue

-- | Encode and decode zero or more header line values for a particular name.
--
-- E.g., @__name__: __value0__@ and @__name__: __value1__@, or just no mention
-- of @__name__@ at all.
headerMany
   :: (ToHeaderValue x, FromHeaderValue o)
   => H.HeaderName
   -- ^ Header name.
   -> (i -> [x])
   -- ^ @'headerMany' k f == 'lmap' f ('headerMany' k 'id')@, provided just for
   -- convenience.
   -> HeaderCodec i [o]
headerMany k f =
   HeaderCodec $ liftAp $ HeaderF k $ lmap f $ headerValuesMany headerValue

-- | Encode and decode one or more header string values for a particular name.
--
-- E.g., @__name__: __value0__@ and @__name__: __value1__@.
headerSome
   :: (ToHeaderValue x, FromHeaderValue o)
   => H.HeaderName
   -- ^ Header name.
   -> (i -> NEL.NonEmpty x)
   -- ^ @'headerSome' k f == 'lmap' f ('headerSome' k 'id')@, provided just for
   -- convenience.
   -> HeaderCodec i (NEL.NonEmpty o)
headerSome k f =
   HeaderCodec $ liftAp $ HeaderF k $ lmap f $ headerValuesSome headerValue

--------------------------------------------------------------------------------

class ToHeaderValue i where
   -- | Render @i@ into a single literal header value.
   --
   -- __WARNING__: The 'B.ByteString' is literally as it is on the header value.
   toHeaderValue :: i -> B.ByteString

class FromHeaderValue o where
   -- | Parse a single literal header value into an @o@.
   --
   -- __WARNING__: The 'B.ByteString' is literally as it is on the header value.
   fromHeaderValue :: B.ByteString -> Maybe o
   default fromHeaderValue :: (Read o) => B.ByteString -> Maybe o
   fromHeaderValue = T.decodeASCII' >=> \t -> readMaybe (T.unpack t)