{-# 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