http-kinder-0.2.0.0: src/Network/HTTP/Kinder/Header/Serialization.hs
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
-- | 'HeaderName's define semantics for 'Text' values seen in HTTP headers
-- over the wire. This module provides classes to map both to and from
-- these reprsentations.
module Network.HTTP.Kinder.Header.Serialization (
-- * Classes for encoding and decoding
HeaderEncode (..)
, HeaderDecode (..)
-- ** Listing constraints to type-level lists
, AllHeaderEncodes
, AllHeaderDecodes
-- * Extra serialization utilities
, headerEncodePair
, headerEncodeBS
, headerDecodeBS
-- * Utilities for writing serialization instances
, displaySetOpt
, uniqueSet
, required
, withDefault
) where
import qualified Data.ByteString as S
import Data.CaseInsensitive (CI)
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Singletons
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import Data.Time
import GHC.Exts
import Network.HTTP.Kinder.Common
import Network.HTTP.Kinder.Header.Definitions
import Network.HTTP.Kinder.Verb
import Network.HTTP.Media (MediaType, Quality)
import qualified Network.HTTP.Media as Media
-- | Determines a 'Text' representation for some value to be encoded as
-- a value of a given 'HeaderName'. Any proxy can be passed as the first
-- argument, although 'Sing' is a nice one to choose. Encodings may choose
-- to not be represented on the wire at all as desired by returning
-- 'Nothing'. This implies default behavior.
class HeaderEncode (n :: HeaderName) a where
headerEncode :: sing n -> a -> Maybe Text
-- | For a given concrete type @a@, a list of pairs @ts@ satisfies
-- @'AllHeaderEncode' a ts@ if each @(n, a)@ in @ts@ has @'HeaderEncode'
-- n a@.
type family AllHeaderEncodes hs :: Constraint where
AllHeaderEncodes '[] = ()
AllHeaderEncodes ( '(n, a) ': hs ) = (HeaderEncode n a, AllHeaderEncodes hs)
-- | Encode a 'HeaderName' singleton and a 'HeaderEncode'-represented value
-- as a pair of name and representation, ready to be sent over the wire.
headerEncodePair
:: forall a (n :: HeaderName)
. HeaderEncode n a => Sing n -> a -> Maybe (CI S.ByteString, S.ByteString)
headerEncodePair s a = do
bs <- headerEncodeBS s a
return (headerName s, bs)
-- | While the semantics of HTTP headers are built off of 'Text'-like
-- values, usually we require a 'S.ByteString' for emission. This helper
-- function converts a header value directly to a 'S.ByteString'.
headerEncodeBS :: HeaderEncode n a => sing n -> a -> Maybe S.ByteString
headerEncodeBS s = fmap Text.encodeUtf8 . headerEncode s
-- | Interprets a (possibly missing) 'Text' representation for some value
-- taking semantics at a given 'HeaderName'. Any proxy can be passed as the
-- first argument, although 'Sing' is a nice one to choose. If a value is
-- expected and no representation is provided then 'Nothing' can be passed
-- seeking a default value (should one exist).
class HeaderDecode (n :: HeaderName) a where
headerDecode :: sing n -> Maybe Text -> Either String a
-- | For a given concrete type @a@, a list of pairs @ts@ satisfies
-- @'AllHeaderDecode' a ts@ if each @(n, a)@ in @ts@ has @'HeaderDecode'
-- n a@.
type family AllHeaderDecodes hs :: Constraint where
AllHeaderDecodes '[] = ()
AllHeaderDecodes ( '(n, a) ': hs ) = (HeaderDecode n a, AllHeaderDecodes hs)
-- | While HTTP header semantics are built off of 'Text'-like values, we
-- usually read a raw 'S.ByteString' from the wire. This helper function
-- combines a 'HeaderDecode' with a UTF-8 decode so as to attempt to
-- deserialize header values directly from a 'S.ByteString'.
headerDecodeBS :: HeaderDecode n a => sing n -> Maybe S.ByteString -> Either String a
headerDecodeBS proxy mays =
case mays of
Nothing -> headerDecode proxy Nothing
Just s ->
case Text.decodeUtf8' s of
Left err -> Left (show err)
Right t -> headerDecode proxy (Just t)
-- Instances/Encoding
-- ----------------------------------------------------------------------------
-- | Output a set of text items as a comma-delimited list OR return nothing
-- if the set is empty
displaySetOpt :: Set Text -> Maybe Text
displaySetOpt s
| Set.null s = Nothing
| otherwise = Just (Text.intercalate "," (Set.toList s))
-- | Extend a 'HeaderEncode' instance on @'Set' v@ to @[v]@.
uniqueSet :: (Ord v, HeaderEncode n (Set v)) => sing n -> [v] -> Maybe Text
uniqueSet s = headerEncode s . Set.fromList
-- | Reports a "raw" value without interpretation
instance HeaderEncode n (Raw Text) where
headerEncode _ (Raw t) = Just t
instance HeaderEncode 'Allow (Set Verb) where
headerEncode _ = displaySetOpt . Set.map verbName
instance HeaderEncode 'Allow [Verb] where
headerEncode = uniqueSet
instance HeaderEncode 'AccessControlExposeHeaders (Set SomeHeaderName) where
headerEncode _ = displaySetOpt . Set.map headerName' where
headerName' (SomeHeaderName h) = headerName h
instance HeaderEncode 'AccessControlExposeHeaders [SomeHeaderName] where
headerEncode = uniqueSet
instance HeaderEncode 'AccessControlAllowHeaders (Set SomeHeaderName) where
headerEncode _ = displaySetOpt . Set.map headerName' where
headerName' (SomeHeaderName h) = headerName h
instance HeaderEncode 'AccessControlAllowHeaders [SomeHeaderName] where
headerEncode = uniqueSet
instance HeaderEncode 'AccessControlMaxAge NominalDiffTime where
headerEncode _ ndt = Just $ Text.pack (show (round ndt :: Int))
instance HeaderEncode 'AccessControlAllowOrigin Text where
headerEncode _ org = Just org
instance HeaderEncode 'AccessControlAllowMethods (Set Verb) where
headerEncode _ = displaySetOpt . Set.map verbName
instance HeaderEncode 'AccessControlAllowMethods [Verb] where
headerEncode = uniqueSet
instance HeaderEncode 'AccessControlAllowCredentials Bool where
headerEncode _ ok = Just (if ok then "true" else "false")
instance HeaderEncode 'ContentType MediaType where
headerEncode _ mt =
case Text.decodeUtf8' (Media.renderHeader mt) of
Left _err -> Nothing
Right txt -> Just txt
-- | Any value can be forced as optional if desired
instance HeaderEncode h t => HeaderEncode h (Maybe t) where
headerEncode p v = v >>= headerEncode p
-- Instances/Decoding
-- ----------------------------------------------------------------------------
-- | Fail to decode if there is no header. For headers which lack default
-- values. If a header lacks a natural default then avoiding failure should
-- be /explicitly/ requested in the types by wrapping it with a 'Maybe'.
required :: (Text -> Either String a) -> Maybe Text -> Either String a
required _ Nothing = Left "missing header value"
required f (Just t) = f t
-- | For headers with natural notions of default values.
withDefault :: a -> (Text -> Either String a) -> (Maybe Text -> Either String a)
withDefault def _ Nothing = Right def
withDefault _ f (Just a) = f a
instance HeaderDecode Accept [Quality MediaType] where
headerDecode _ = withDefault [] parser where
parser txt =
case Media.parseQuality (Text.encodeUtf8 txt) of
Nothing -> Left "malformed accept header"
Just mts -> Right mts
instance HeaderDecode ContentType MediaType where
headerDecode _ = required $ \txt ->
case Media.parseAccept (Text.encodeUtf8 txt) of
Nothing -> Left "malformed content type"
Just ct -> Right ct
-- | Returns the raw header value
instance HeaderDecode n (Raw Text) where
headerDecode _ = required $ \text -> Right (Raw text)
-- | Any value may be only optionally captured as desired
instance HeaderDecode h t => HeaderDecode h (Maybe t) where
headerDecode _ Nothing = Right Nothing
headerDecode p (Just t) = fmap Just (headerDecode p (Just t))