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)