packages feed

ihttp-0.3.0: Network/IHttp/Types.hs

-- |
-- Module:     Network.IHttp.Types
-- Copyright:  (c) 2010 Ertugrul Soeylemez
-- License:    BSD3
-- Maintainer: Ertugrul Soeylemez <es@ertes.de>
-- Stability:  beta
--
-- Types for ihttp.

{-# LANGUAGE DeriveDataTypeable, GeneralizedNewtypeDeriving #-}

module Network.IHttp.Types
    ( -- * Protocol data
      HeaderMap,
      HttpMethod(..),
      HttpVersion(..),
      Request(..),
      Response(..),

      -- * Header keys
      HeaderKey(..),
      headerKey,
      recaseHeaderKey,

      -- * Miscellaneous types
      HttpError(..)
    )
    where

import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as BC
import Control.Arrow
import Control.Exception as Ex
import Data.ByteString.Char8 (ByteString)
import Data.Data
import Data.Map (Map)
import Data.Monoid
import Data.String
import Text.Printf


-- TODO: Common HTTP headers with correct types.

-- data CommonHeaders =
--     CommonHeaders {
--       contentCharsetHeader :: Maybe ByteString,  -- ^ Document character set.
--       contentLengthHeader  :: Maybe Integer,     -- ^ Length of document in bytes.
--       contentTypeHeader    :: Maybe ByteString   -- ^ MIME type of document.
--     }


-- | A HTTP header key.  This is just a 'ByteString', but with
-- case-insensitive ordering and a pretty 'IsString' instance.

newtype HeaderKey = HeaderKey { getHeaderKey :: ByteString }
    deriving (Data, Monoid, Read, Typeable)

instance Eq HeaderKey where
    HeaderKey str1 == HeaderKey str2 =
        B.map wordToUpper str1 == B.map wordToUpper str2

instance IsString HeaderKey where
    fromString = HeaderKey . recaseHeaderKey . BC.pack

instance Ord HeaderKey where
    compare (HeaderKey str1) (HeaderKey str2) =
        compare (caseConvert str1) (caseConvert str2)

        where
        caseConvert :: ByteString -> ByteString
        caseConvert = B.map wordToUpper

instance Show HeaderKey where
    show = ("HeaderKey " ++) . show . getHeaderKey


-- | Map of HTTP headers.

type HeaderMap = Map HeaderKey ByteString


-- | HTTP error.

data HttpError
    -- | Non-OK response.
    = ResponseNotOkError { httpErrorResponse :: Response }

    -- | Invalid headers from client/server.
    | InvalidHeaderError { httpErrorMessage :: String }

    -- | Invalid requests from client.
    | InvalidRequestError { httpErrorMessage :: String }

    -- | Invalid responses from server.
    | InvalidResponseError { httpErrorMessage :: String }

    -- | Unsupported HTTP version.
    | UnsupportedVersionError { httpErrorMessage :: String }
    deriving (Eq, Typeable)

instance Exception HttpError

instance Show HttpError where
    show (ResponseNotOkError resp)     =
        let code = responseCode resp
            msg  = responseMessage resp
        in printf "Non-OK response: %i %s" code (BC.unpack msg)
    show (InvalidHeaderError msg)      = "Invalid HTTP header: " ++ msg
    show (InvalidRequestError msg)     = "Invalid HTTP request: " ++ msg
    show (InvalidResponseError msg)    = "Invalid HTTP response: " ++ msg
    show (UnsupportedVersionError msg) = "Unsupported HTTP version: " ++ msg


-- | HTTP request method.

data HttpMethod
    = ConnectMethod  -- ^ CONNECT
    | DeleteMethod   -- ^ DELETE
    | GetMethod      -- ^ GET
    | HeadMethod     -- ^ HEAD
    | OptionsMethod  -- ^ OPTIONS
    | PatchMethod    -- ^ PATCH
    | PostMethod     -- ^ POST
    | PutMethod      -- ^ PUT
    | TraceMethod    -- ^ TRACE
    | XMethod ByteString  -- ^ Methods this library doesn't know.
    deriving (Eq, Read, Show)


-- | HTTP protocol version.

data HttpVersion
    = Http1_0  -- ^ Version 1.0 of HTTP.
    | Http1_1  -- ^ Version 1.1 of HTTP.
    deriving (Eq, Ord, Read, Show)


-- | HTTP request line with status code.

data Request =
    Request {
      requestHeaders :: HeaderMap,   -- ^ Request headers.
      requestMethod  :: HttpMethod,  -- ^ Request method.
      requestUri     :: ByteString,  -- ^ Request URI.
      requestVersion :: HttpVersion  -- ^ HTTP version of request.
    }
    deriving (Eq, Read, Show)


-- | HTTP response line with the status code.

data Response =
    Response {
      responseCode    :: Int,         -- ^ HTTP response code.
      responseHeaders :: HeaderMap,   -- ^ Response headers.
      responseMessage :: ByteString,  -- ^ Response message.
      responseVersion :: HttpVersion  -- ^ Protocol version of response.
    }
    deriving (Eq, Read, Show)


-- | Turn a 'ByteString' with 'recaseHeaderKey' case conversion to a
-- 'HeaderKey'.

headerKey :: ByteString -> HeaderKey
headerKey = HeaderKey . recaseHeaderKey


-- | Convert an arbitrary case header key to dashed camel case
-- ("content-type" -> "Content-Type").

recaseHeaderKey :: ByteString -> ByteString
recaseHeaderKey =
    B.intercalate (BC.singleton '-') .
    map (maybe B.empty (uncurry B.cons . (wordToUpper *** B.map wordToLower)) .
         B.uncons) .
    BC.split '-'


-- | Convert upper case characters to lower case (ASCII).

wordToLower :: (Num a, Ord a) => a -> a
wordToLower c = if c >= 65 && c <= 90 then c + 32 else c


-- | Convert lower case characters to upper case (ASCII).

wordToUpper :: (Num a, Ord a) => a -> a
wordToUpper c = if c >= 97 && c <= 122 then c - 32 else c