{-# 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))