webcrank-0.1: src/Webcrank/Internal/Types.hs
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
module Webcrank.Internal.Types where
import Control.Applicative
import Control.Lens
import Control.Monad.Catch
import Control.Monad.RWS
import Control.Monad.Trans.Either
import Control.Monad.Trans.Maybe
import Data.ByteString (ByteString)
import qualified Data.ByteString.Lazy as LB
import qualified Data.ByteString.UTF8 as B
import Data.CaseInsensitive (CI)
import Data.HashMap.Strict (HashMap)
import Data.List.NonEmpty (NonEmpty)
import Data.Text (Text)
import Network.HTTP.Date
import Network.HTTP.Media
import Network.HTTP.Types
import Webcrank.Internal.Headers
-- | A dictionary of functions that Webcrank needs in order to make decisions.
data ServerAPI m = ServerAPI
{ srvGetRequestMethod :: m Method
-- ^ Get the request method of the current request.
, srvGetRequestURI :: m ByteString
-- ^ The full URI of the request.
, srvGetRequestHeader :: HeaderName -> m (Maybe ByteString)
-- ^ Get the request header of the current request.
, srvGetRequestTime :: m HTTPDate
-- ^ Get the time the request was received.
}
type HeadersMap = HashMap HeaderName [ByteString]
-- | Content coding type, e.g. gzip, decompress. See @'encodingsProvided'@.
type Encoding = CI ByteString
-- | Character set type, e.g. utf-8. See @'charsetsProvided'@.
type Charset = CI ByteString
-- | Response body type.
type Body = LB.ByteString
-- | Indicates whether client is authorized to perform the requested
-- operation on the resource. See @'isAuthorized'@.
data Authorized
= Authorized
-- ^ Tells Webcrank that the client is authorized to perform the
-- requested operation on the resource.
| Unauthorized ByteString
-- ^ Tells Webcrank that the client is not authorized to perform
-- the operation on the resource. The value is sent in the
-- @WWW-Authenticate@ header of the response,
-- e.g. @Basic realm="Webcrank"@.
-- | Indicates whether the resource supports multiple character sets
-- or not. See @'charsetsProvided'@
data CharsetsProvided
= NoCharset
-- ^ Indicates that the resource doesn't support any additional
-- character sets, all responses from the resource will have the
-- same character set, regardless of what the client requests.
| CharsetsProvided (NonEmpty (Charset, Body -> Body))
-- ^ The character sets the resource supports along with functions
-- for converting the response body.
-- | Weak or strong entity tags as used in HTTP ETag and @If-*-Match@ headers.
data ETag = StrongETag ByteString | WeakETag ByteString deriving Eq
instance Show ETag where
show e = B.toString $ case e of
StrongETag v -> "\"" <> v <> "\""
WeakETag v -> "W/\"" <> v <> "\""
instance RenderHeader ETag where
renderHeader = \case
StrongETag v -> quotedString v
WeakETag v -> "W/" <> quotedString v
data Halt = Halt Status | Error Status LB.ByteString
deriving (Eq, Show)
-- | Monad transformer for @'Resource'@ functions which can halt the request
-- processing early with an error or some other response. Values are created with
-- the smart constructors @'werror'@ and @'halt'@.
newtype HaltT m a = HaltT { unHaltT :: EitherT Halt m a }
deriving
( Functor
, Applicative
, Monad
, MonadIO
, MonadTrans
, MonadReader r
, MonadState s
, MonadWriter w
, MonadThrow
, MonadCatch
)
-- | How @POST@ requests should be treated. See @'postAction'@.
data PostAction m
= PostCreate [Text]
-- ^ Treat @POST@s as creating new resources and respond
-- with @201 Created@, with the given path in the Location header.
| PostCreateRedir [Text]
-- ^ Treat @POST@s as creating new resources and respond with
-- @301 See Other@, redirecting the client to the new resource.
| PostProcess (HaltT m ())
-- ^ Treat @POST@s as a process which is executed without redirect.
| PostProcessRedir (HaltT m ByteString)
-- ^ Treat @POST@s as a process and redirect the client to a
-- different (possibly new) resource.
data LogData = LogData
instance Monoid LogData where
mempty = LogData
mappend _ _ = LogData
-- | A @Resource@ is a dictionary of functions which are used in the Webcrank
-- decision process to determine how requests should be handled.
--
-- Each function has a type of either @m a@ or @'HaltT' m a@.
-- A resource function which yields a @HaltT m a@ value allows the function
-- to terminate the request processing early using @'halt'@ or
-- @'werror'@.
--
-- The defaults documented are used by the @'resource'@ smart constructor.
-- A resource that responds to @GET@ requests with an HTML response would be
-- written as
--
-- @
-- myResource = resource { contentTypesProvided = return $ [("text/html", return "Hello world!")] }
-- @
--
-- @'responseWithBody'@ and @'responseWithHtml'@ are additional
-- smart constructors useful creating resources.
data Resource m = Resource
{ serviceAvailable :: HaltT m Bool
-- ^ @False@ will result in @503 Service Unavailable@. Defaults to @True@.
, uriTooLong :: HaltT m Bool
-- ^ @True@ will result in @414 Request Too Long@. Defaults to @False@.
, allowedMethods :: m [Method]
-- ^ If a @Method@ not in this list is requested, then a @405 Method Not
-- Allowed@ will be sent. Defaults to @["GET", "HEAD"]@.
, malformedRequest :: HaltT m Bool
-- ^ @True@ will result in @400 Bad Request@. Defaults to @False@.
, isAuthorized :: HaltT m Authorized
-- ^ If @Authorized@, the response will be @401 Unauthorized@.
-- @Unauthorized@ will be used as the challenge in the @WWW-Authenticate@
-- header, e.g. @Basic realm="Webcrank"@.
-- Defaults to @Authorized@.
, forbidden :: HaltT m Bool
-- ^ @True@ will result in @403 Forbidden@. Defaults to @False@.
, validContentHeaders :: HaltT m Bool
-- ^ @False@ will result in @501 Not Implemented@. Defaults to @True@.
, knownContentType :: HaltT m Bool
-- ^ @False@ will result in @415 Unsupported Media Type@. Defaults to
-- @True@.
, validEntityLength :: HaltT m Bool
-- ^ @False@ will result in @413 Request Entity Too Large@. Defaults to
-- @True@.
, options :: m ResponseHeaders
-- ^ If the OPTIONS method is supported and is used, the headers that
-- should appear in the response. Defaults to @[]@.
, contentTypesProvided :: m [(MediaType, HaltT m Body)]
-- ^ Content negotiation is driven by this function. For example, if a
-- client request includes an @Accept@ header with a value that does not
-- appear as a @MediaType@ in any of the tuples, then a @406 Not
-- Acceptable@ will be sent. If there is a matching @MediaType@, that
-- function is used to create the entity when a response should include one.
-- Defaults to @[]@.
, charsetsProvided :: m CharsetsProvided
-- ^ Used on GET requests to ensure that the entity is in @Charset@.
-- Defaults to @NoCharset@.
, encodingsProvided :: m [(Encoding, Body -> Body)]
-- ^ Used on GET requests to ensure that the body is encoded.
-- One useful setting is to have the function check on method, and on GET
-- requests return @[("identity", id), ("gzip", compress)]@ as this is all
-- that is needed to support gzip content encoding. Defaults to
-- @[]@.
, resourceExists :: HaltT m Bool
-- ^ @False@ will result in @404 Not Found@. Defaults to @True@.
, generateETag :: MaybeT m ETag
-- ^ If this returns an @ETag@, it will be used for the ETag header and for
-- comparison in conditional requests. Defaults to @mzero@.
, lastModified :: MaybeT m HTTPDate
-- ^ If this returns a @HTTPDate@, it will be used for the Last-Modified header
-- and for comparison in conditional requests. Defaults to @mzero@.
, expires :: MaybeT m HTTPDate
-- ^ If this returns a @HTTPDate@, it will be used for the Expires header.
-- Defaults to @mzero@.
, movedPermanently :: MaybeT (HaltT m) ByteString
-- ^ If this returns a URI, the client will receive a 301 Moved Permanently
-- with the URI in the Location header. Defaults to @mzero@.
, movedTemporarily :: MaybeT (HaltT m) ByteString
-- ^ If this returns a URI, the client will receive a 307 Temporary Redirect
-- with URI in the Location header. Defaults to @mzero@.
, previouslyExisted :: HaltT m Bool
-- ^ If this returns @True@, the @movedPermanently@ and @movedTemporarily@
-- callbacks will be invoked to determine whether the response should be
-- 301 Moved Permanently, 307 Temporary Redirect, or 410 Gone. Defaults
-- to @False@.
, allowMissingPost :: HaltT m Bool
-- ^ If the resource accepts POST requests to nonexistent resources, then
-- this should return @True@. Defaults to @False@.
, deleteResource :: HaltT m Bool
-- ^ This is called when a DELETE request should be enacted, and should return
-- @True@ if the deletion succeeded or has been accepted. Defaults to
-- @True@.
, deleteCompleted :: HaltT m Bool
-- ^ This is only called after a successful @deleteResource@ call, and should
-- return @False@ if the deletion was accepted but cannot yet be guaranteed to
-- have finished. Defaults to @True@.
, postAction :: m (PostAction m)
-- ^ If POST requests should be treated as a request to put content into a
-- (potentially new) resource as opposed to being a generic submission for
-- processing, then this function should return @PostCreate path@. If it
-- does return @PostCreate path@, then the rest of the request will be
-- treated much like a PUT to the path entry. Otherwise, if it returns
-- @PostProcess a@, then the action @a@ will be run. Defaults to
-- @PostProcess $ return ()@.
, contentTypesAccepted :: m [(MediaType, HaltT m ())]
-- ^ This is used similarly to @contentTypesProvided@, except that it is
-- for incoming resource representations -- for example, @PUT@ requests.
-- Handler functions usually want to use server specific functions to
-- access the incoming request body. Defaults to @[]@.
, variances :: m [HeaderName]
-- ^ This function should return a list of strings with header names that
-- should be included in a given response's Vary header. The standard
-- conneg headers (Accept, Accept-Encoding, Accept-Charset,
-- Accept-Language) do not need to be specified here as Webcrank will add
-- the correct elements of those automatically depending on resource
-- behavior. Defaults to @[]@.
, multipleChoices :: HaltT m Bool
-- ^ If this returns @True@, then it is assumed that multiple
-- representations of the response are possible and a single one cannot
-- be automatically chosen, so a @300 Multiple Choices@ will be sent
-- instead of a @200 OK@. Defaults to @False@.
, isConflict :: m Bool
-- ^ If this returns @True@, the client will receive a 409 Conflict.
-- Defaults to @False@.
, finishRequest :: m ()
-- ^ Called just before the final response is constructed and sent.
}
-- | A wrapper for the @'ServerAPI'@ and @'Resource'@ that should be used
-- to process requests to a path.
data ResourceData m = ResourceData
{ _resourceDataServerAPI :: ServerAPI m
, _resourceDataResource :: Resource m
}
makeClassy ''ResourceData
-- | Container used to keep track of the decision state and what is known
-- about response while processing a request.
data ReqData = ReqData
{ _reqDataRespMediaType :: MediaType
, _reqDataRespCharset :: Maybe Charset
, _reqDataRespEncoding :: Maybe Encoding
, _reqDataDispPath :: [Text]
, _reqDataRespHeaders :: HeadersMap
, _reqDataRespBody :: Maybe Body
}
makeClassy ''ReqData