yesod/Web/Restful/Response.hs
2009-11-13 15:17:35 +02:00

196 lines
5.6 KiB
Haskell

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeSynonymInstances #-} -- FIXME remove
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE DeriveDataTypeable #-}
---------------------------------------------------------
--
-- Module : Web.Restful.Response
-- Copyright : Michael Snoyman
-- License : BSD3
--
-- Maintainer : Michael Snoyman <michael@snoyman.com>
-- Stability : Stable
-- Portability : portable
--
-- Generating responses.
--
---------------------------------------------------------
module Web.Restful.Response
( Response (..)
-- * Representations
, RepT
, chooseRep
, HasReps (..)
, ContentType
-- * Content
, Content
, ToContent (..)
, runContent
, lbsContent
, translateContent
-- * Abnormal responses
, ErrorResult (..)
, getHeaders
, getStatus
-- * Header
, Header (..)
, toPair
-- * Generic responses
, genResponse
, htmlResponse
, objectResponse
-- * Tests
, testSuite
) where
import Data.Time.Clock
import Data.Object
import Data.Object.Raw
import Data.Object.Translate
import Data.Object.Instances
import qualified Data.ByteString.Lazy as LBS
import Data.ByteString.Class
import qualified Data.Text.Lazy as LT
import qualified Data.Text.Lazy.Encoding as LTE
import Web.Encodings (formatW3)
import Test.Framework (testGroup, Test)
import Data.Generics
import Control.Exception (Exception)
import Data.Maybe (fromJust)
data Response = Response Int [Header] ContentType Content
type ContentType = String
data Content = ByteString LBS.ByteString
| Text LT.Text
| TransText ([Language] -> LT.Text)
runContent :: [Language] -> Content -> LBS.ByteString
runContent _ (ByteString lbs) = lbs
runContent _ (Text lt) = LTE.encodeUtf8 lt
runContent ls (TransText t) = LTE.encodeUtf8 $ t ls
class ToContent a where
toContent :: a -> Content
instance ToContent LBS.ByteString where
toContent = ByteString
instance ToContent String where
toContent = Text . LT.pack
instance ToContent ([Language] -> String) where
toContent f = TransText $ LT.pack . f
instance ToContent Translator where
toContent = TransText
lbsContent :: LazyByteString lbs => lbs -> Content
lbsContent = ByteString . toLazyByteString
translateContent :: CanTranslate t => t -> Content
translateContent t = toContent $ translate t
type RepT m = (ContentType, m Content)
chooseRep :: Monad m
=> [ContentType]
-> [RepT m]
-> RepT m
chooseRep cs rs
| null rs = error "All reps must have at least one representation" -- FIXME
| otherwise = do
let availCs = map fst rs
case filter (`elem` availCs) cs of
[] -> head rs
[ctype] -> (ctype, fromJust $ lookup ctype rs) -- FIXME
_ -> error "Overlapping representations" -- FIXME just take the first?
-- | Something which can be represented as multiple content types.
-- Each content type is called a representation of the data.
class Monad m => HasReps a m where
-- | Provide an ordered list of possible representations, depending on
-- content type. If the user asked for a specific response type (like
-- text/html), then that will get priority. If not, then the first
-- element in this list will be used.
reps :: a -> [RepT m]
-- | Abnormal return codes.
data ErrorResult =
Redirect String
| NotFound
| InternalError String
| InvalidArgs [(String, String)]
| PermissionDenied
deriving (Show, Typeable)
instance Exception ErrorResult
getStatus :: ErrorResult -> Int
getStatus (Redirect _) = 303
getStatus NotFound = 404
getStatus (InternalError _) = 500
getStatus (InvalidArgs _) = 400
getStatus PermissionDenied = 403
getHeaders :: ErrorResult -> [Header]
getHeaders (Redirect s) = [Header "Location" s]
getHeaders _ = []
----- header stuff
-- | Headers to be added to a 'Result'.
data Header =
AddCookie Int String String
| DeleteCookie String
| Header String String
-- | Convert Header to a key/value pair.
toPair :: Header -> IO (String, String)
toPair (AddCookie minutes key value) = do
now <- getCurrentTime
let expires = addUTCTime (fromIntegral $ minutes * 60) now
return ("Set-Cookie", key ++ "=" ++ value ++"; path=/; expires="
++ formatW3 expires)
toPair (DeleteCookie key) = return
("Set-Cookie",
key ++ "=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT")
toPair (Header key value) = return (key, value)
------ Generic responses
-- FIXME move these to Handler?
-- | Return a response with an arbitrary content type.
genResponse :: (Monad m, LazyByteString lbs)
=> ContentType
-> lbs
-> [RepT m]
genResponse ct lbs = [(ct, return $ lbsContent lbs)]
-- | Return a response with a text/html content type.
htmlResponse :: (Monad m, LazyByteString lbs) => lbs -> [RepT m]
htmlResponse = genResponse "text/html"
-- | Return a response from an Object. FIXME use TextObject
objectResponse :: (Monad m, ToObject o Raw Raw) => o -> [RepT m]
objectResponse = reps . toRawObject
-- HasReps instances
instance Monad m => HasReps () m where
reps _ = [("text/plain", return $ lbsContent "")]
instance Monad m => HasReps RawObject m where -- FIXME TextObject
reps o =
[ ("text/html", return $ lbsContent $ unHtml $ safeFromObject o)
, ("application/json", return $ lbsContent $ unJson $ safeFromObject o)
, ("text/yaml", return $ lbsContent $ unYaml $ safeFromObject o)
]
{- FIXME
instance HasReps (Reps m) where
reps = id
-}
----- Testing
testSuite :: Test
testSuite = testGroup "Web.Restful.Response"
[
]