http-media-0.1.1: src/Network/HTTP/Media.hs
------------------------------------------------------------------------------
-- | A framework for parsing HTTP media type headers.
module Network.HTTP.Media
(
-- * Media types
MediaType
, (//)
, (/:)
, mainType
, subType
, parameters
, (/?)
, (/.)
, Quality
-- * Parsing
, parseAccept
-- * Accept matching
, matchAccept
, mapAccept
-- * Content matching
, matchContent
, mapContent
-- * Match
, Match (..)
) where
------------------------------------------------------------------------------
import qualified Data.ByteString as BS
------------------------------------------------------------------------------
import Control.Applicative (pure, (<*>), (<|>))
import Control.Monad (guard)
import Data.ByteString (ByteString, split)
import Data.ByteString.UTF8 (toString)
------------------------------------------------------------------------------
import Network.HTTP.Media.Match as Match
import Network.HTTP.Media.MediaType as MediaType
import Network.HTTP.Media.Quality
import Network.HTTP.Media.Utils
------------------------------------------------------------------------------
-- | Parses a full Accept header into a list of quality-valued media types.
parseAccept :: ByteString -> Maybe [Quality MediaType]
parseAccept = (. split comma) . mapM $ \bs ->
let (accept, q) = BS.breakSubstring ";q=" $ BS.filter (/= space) bs
in (<*> parse accept) $ if BS.null q
then pure maxQuality else fmap (flip Quality) $ readQ
(toString $ BS.takeWhile (/= semi) $ BS.drop 3 q)
------------------------------------------------------------------------------
-- | Matches a list of server-side resource options against a quality-marked
-- list of client-side preferences. A result of 'Nothing' means that nothing
-- matched (which should indicate a 406 error). If two or more results arise
-- with the same quality level and specificity, then the first one in the
-- server list is chosen.
--
-- The use of the 'Match' type class allows the application of either
-- 'MediaType' for the standard Accept header or 'ByteString' for any other
-- Accept header which can be marked with a quality value. The standard
-- application of this function for 'MediaType' should be in conjunction with
-- 'parseAccepts'.
--
-- > parseAccepts header >>= matchQuality resourceTypeOptions
--
-- For more information on the matching process see RFC 2616, section 14.
matchAccept
:: Match a
=> [a] -- ^ The server-side options
-> [Quality a] -- ^ The client-side preferences
-> Maybe a
matchAccept server clientq = guard (hq /= 0) >> specific qs
where
merge (Quality c q) = map (`Quality` q) $ filter (`matches` c) server
matched = concatMap merge clientq
(hq, qs) = foldr qfold (0, []) matched
qfold (Quality v q) (mq, vs) = case compare q mq of
GT -> (q, [v])
EQ -> (mq, v : vs)
LT -> (mq, vs)
specific (a : ms) = Just $ foldl mostSpecific a ms
specific [] = Nothing
------------------------------------------------------------------------------
-- | The equivalent of 'matchAccept' above, except the resulting choice is
-- mapped to another value. Convenient for specifying how to translate the
-- resource into each of its available formats.
--
-- > maybe render406Error renderResource $ parseAccepts header >>= mapQuality
-- > [ ("text/html", asHtml)
-- > , ("application/json", asJson)
-- > ]
mapAccept
:: Match a
=> [(a, b)] -- ^ The map of server-side preferences to values
-> [Quality a] -- ^ The client-side preferences
-> Maybe b
mapAccept s c = matchAccept (map fst s) c >>= lookupMatches s
------------------------------------------------------------------------------
-- | Matches a list of server-side parsing options against a the client-side
-- content value. A result of 'Nothing' means that nothing matched (which
-- should indicate a 415 error).
--
-- As with the Accept parsing, he use of the 'Match' type class allows the
-- application of either 'MediaType' or 'ByteString'.
matchContent
:: Match a
=> a -- ^ The client's request value
-> [a] -- ^ The server-side response options
-> Maybe a
matchContent client = foldl choose Nothing
where choose m server = m <|> (guard (matches client server) >> Just server)
------------------------------------------------------------------------------
-- | The equivalent of 'matchContent' above, except the resulting choice is
-- mapped to another value.
mapContent
:: Match a
=> a -- ^ The client request's header value
-> [(a, b)] -- ^ The map of server-side responses
-> Maybe b
mapContent c s = matchContent c (map fst s) >>= lookupMatches s
------------------------------------------------------------------------------
-- | The equivalent of 'lookupBy matches'.
lookupMatches :: Match a => [(a, b)] -> a -> Maybe b
lookupMatches ((k, v) : r) a
| Match.matches k a = Just v
| otherwise = lookupMatches r a
lookupMatches [] _ = Nothing