snap-language-0.1.0.0: src/Snap/Language.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
-- |
-- Language handling for Snap.
--
-- Support for determining the client's prefered language using
-- the Accept-Language header or using suffixes to the requested URI.
module Snap.Language
( RangeMapping
, getAcceptLanguage
, getSuffixLanguage
, switchSuffixLanguage
, setContentLanguage
) where
import Data.Attoparsec.ByteString.Char8(parseOnly,
string,
double,
Parser,
letter_ascii,
many1,
many',
char,
option,
eitherP,
sepBy,
skipSpace,
endOfLine)
import Data.ByteString (ByteString,
isSuffixOf)
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as BC
import Data.Char (toLower)
import Data.List (intersperse,isPrefixOf,find)
import Control.Applicative ((*>),(<$>),(<*>),(<|>))
import Snap.Core (getsRequest,
getRequest,
putRequest,
getHeader,
MonadSnap,
Cookie(..),
addResponseCookie,
modifyResponse,
getCookie,
setHeader,
pass,
rqPathInfo)
import Data.Maybe (mapMaybe,
listToMaybe)
import Data.Map (Map,
toList)
import Data.Tuple (swap)
range :: Parser String
range = (++) <$> mletters <*> (fmap concat $ many' $ (:) <$> (char '-') <*> mletters)
where mletters = many1 letter_ascii
rangeval :: Parser (Maybe String, Double)
rangeval =
do
r <- eitherP (char '*') range
q <- option 1 $ string ";q=" *> double
return (either (const Nothing) Just r,q)
acceptLanguageParser :: Parser [(Maybe String, Double)]
acceptLanguageParser = skipSpace *> (sepBy rangeval $ skipSpace *> char ',' <* skipSpace)
matches :: String
-> Maybe String
-> Bool
matches _ Nothing = True
matches provided (Just requested) =
(map toLower requested) `isPrefixOf` (map toLower provided)
candidates :: Map String a
-> [(Maybe String, Double)]
-> [(a,Double)]
candidates provided requested = concatMap go $ toList provided
where go (range,x) = map (\(a,b) -> (x,b)) $ filter (matches range . fst) requested
pickLanguage' :: Map String a
-> [(Maybe String,Double)]
-> Maybe a
pickLanguage' provided requested = fmap fst $ foldr go Nothing $ candidates provided requested
where go r' Nothing = return r'
go r'@(val',q') (Just r@(val,q)) | q' > q = return r'
| otherwise = return r
pickLanguage :: Map String a
-> ByteString
-> Maybe a
pickLanguage provided headerString =
either (const Nothing) (pickLanguage' provided) $ parseOnly acceptLanguageParser headerString
-- | Attempt to find a suitable language according to the Accept-Language
-- header of the request.
--
-- This handler will call pass if it cannot find a suitable language.
getAcceptLanguage :: MonadSnap m
=> RangeMapping a
-> m a
getAcceptLanguage rangeMapping =
do
al <- getsRequest $ getHeader "Accept-Language"
maybe pass return $ al >>= pickLanguage rangeMapping
-- | A mapping from language ranges as defined in rfc2616 to languages in your own representation.
--
-- For example:
--
-- > data Language = EN | SV deriving Eq
-- >
-- > mapping :: RangeMapping Language
-- > mapping = Map.fromList [("en-gb",EN),("sv-se",SV)]
type RangeMapping a = Map String a
removeSuffix :: ByteString
-> ByteString
-> Maybe ByteString
removeSuffix suf x | suf `B.isSuffixOf` x = Just $ B.take ((B.length x) - (B.length suf)) x
| otherwise = Nothing
suffixes :: RangeMapping a
-> [(ByteString,a)]
suffixes = map go . toList
where go (str,val) = (BC.pack $ '.':str,val)
matchSuffix :: ByteString
-> [(ByteString,a)]
-> Maybe (ByteString,a)
matchSuffix str sfxs = listToMaybe $ mapMaybe go sfxs
where go (sfx,val) = fmap (,val) $ removeSuffix sfx str
-- | Attempt to find a suitable language according to a suffix in the request URI corresponding to a language range.
--
-- Will call pass if it cannot find a suitable language.
--
-- If a match is found, the suffix will be removed from the URI in the request, so that you
-- can later match on your resource as usual and not worry about suffixes.
--
-- For example, with the following requested URI:
--
-- > /resource.en-gb?param=value
--
-- 'getSuffixLanguage' with our previously defined mapping will return 'EN' and 'rqPathInfo' will be changed to:
--
-- > /resource?param=value
getSuffixLanguage :: MonadSnap m
=> RangeMapping a
-> m a
getSuffixLanguage rangeMapping =
do
r <- getRequest
case matchSuffix (rqPathInfo r) $ suffixes rangeMapping of
Nothing -> pass
Just (rqPathInfo',val) ->
do
putRequest $ r { rqPathInfo = rqPathInfo' }
return val
-- | Change, or remove, the language suffix of an URI.
switchSuffixLanguage :: Eq a
=> RangeMapping a
-> ByteString -- ^ The URI.
-> Maybe a -- ^ The language to be appended to the URI, or Nothing to remove language suffix.
-> ByteString
switchSuffixLanguage rangeMapping uri lang = maybe (addSuffix lang path) (addSuffix lang . fst) $ matchSuffix path $ suffixes rangeMapping
where (path,params) = BC.break ((==) '?') uri
addSuffix lang p = B.concat [p,findSfx lang,params]
findSfx Nothing = B.empty
findSfx (Just l) = maybe B.empty id $ lookup l $ map swap $ suffixes rangeMapping
-- | Set the Content-Language header in the response.
setContentLanguage :: (Eq a, MonadSnap m)
=> RangeMapping a
-> a
-> m ()
setContentLanguage rangeMapping val =
maybe (return ()) go $ lookup val $ map swap $ toList rangeMapping
where go = modifyResponse . setHeader "Content-Language" . BC.pack