{-# language DuplicateRecordFields #-}
{-# language DerivingStrategies #-}
{-# language GeneralizedNewtypeDeriving #-}
{-# language OverloadedStrings #-}
-- TODO: Right now, this uses a crummy implementation. Instead, we
-- should hash all the keys and search the hashes first to speed
-- things up.
module Http.Headers
( -- * Types
Headers
, LookupException(..)
-- * Construct
, fromArray
-- * Expose
, toArray
-- * Lookup
, lookup
, lookupFirst
, lookupAll
-- * Specialized Lookup
, lookupContentType
, lookupContentLength
, lookupTransferEncoding
) where
import Prelude hiding (lookup)
import Data.Text (Text)
import Data.Primitive (SmallArray)
import Http.Header (Header(Header))
import Data.Foldable (foldl')
import qualified Data.List as List
import qualified Data.Primitive.Contiguous as C
import qualified Data.Text as T
import qualified Http.Header
-- | Collection of HTTP headers. Supports case-insensitive lookup.
-- This is intended to be used for small collections of headers.
-- Expect degraded performance if this is used for collections of
-- more than 128 headers.
--
-- This preserves the original order of the headers and the original
-- case of the header names.
newtype Headers = Headers (SmallArray Header)
deriving newtype (Show)
-- | Many headers cannot appear more than once. This is part of
-- the return type for 'lookup', and it helps us track whether the
-- lookup failure was the result of something that might be expected
-- (the header was @Missing@) or something that is definitely a mistake
-- (the header was duplicated).
data LookupException
= Duplicate
| Missing
-- | Convert array of headers to a 'Headers' collection that supports
-- efficient lookup.
fromArray :: SmallArray Header -> Headers
fromArray = Headers
-- | Recover the original headers from from the 'Headers' collection.
-- This is @O(1)@ and is most commonly used to fold over the headers.
toArray :: Headers -> SmallArray Header
toArray (Headers xs) = xs
-- | Case insensitive lookup of an HTTP header. If the header is present,
-- returns both the original header name (may differs in case from the
-- header name searched for) and the header value. Only returns the first
-- occurrence of the header.
lookupFirst ::
Text -- header name
-> Headers
-> Maybe Header
lookupFirst needle (Headers hdrs) =
List.find (\Header{name} -> caseInsensitiveEq needle name) hdrs
-- | Lookup a header that should not appear more than one time and verify
-- that it did not occur more than once. If it appears more than once
-- (or less than once), returns a 'LookupException'.
lookup ::
Text -- header name
-> Headers
-> Either LookupException Header
lookup needle hdrs@(Headers xs) = case lookupFirst needle hdrs of
Nothing -> Left Missing
Just hdr ->
let count = foldl'
(\acc Header{name} -> if caseInsensitiveEq needle name
then acc + 1
else acc
) (0 :: Int) xs
in if count > 1 then Left Duplicate else Right hdr
-- | Lookup a header that may appear more than once. Some headers
-- (e.g. @Set-Cookie@, @X-Forwarded-For@) are allowed to appear multiple
-- times. This returns all the headers that matched along with their
-- original names.
lookupAll ::
Text -- header name
-> Headers
-> SmallArray Header
lookupAll needle (Headers hdrs) =
C.filter (\Header{name} -> caseInsensitiveEq needle name) hdrs
-- TODO: Make this not allocate
caseInsensitiveEq :: Text -> Text -> Bool
caseInsensitiveEq a b = T.toLower a == T.toLower b
lookupTransferEncoding :: Headers -> Either LookupException Header
lookupTransferEncoding = lookup "transfer-encoding"
lookupContentType :: Headers -> Either LookupException Header
lookupContentType = lookup "content-type"
lookupContentLength :: Headers -> Either LookupException Header
lookupContentLength = lookup "content-length"