{-# LANGUAGE GADTs #-} -- for 'Router' and 'RouterUnion' {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DataKinds #-} -- for 'BinTree' {-# LANGUAGE DefaultSignatures #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE UndecidableInstances #-} -- for nested type family application, -- eg. in 'BodyStreamConstraint' {-# OPTIONS_GHC -fno-warn-orphans #-} -- | See -- for an example of how to use this module. module Symantic.HTTP.Server where import Control.Applicative (Applicative(..)) import Control.Arrow (first) import Control.Monad (Monad(..), unless, sequence, guard, (=<<)) import Control.Monad.Trans.Class (MonadTrans(..)) import Control.Monad.Trans.Except (ExceptT(..), runExceptT) import Data.Bool import Data.Either (Either(..)) import Data.Eq (Eq(..)) import Data.Function (($), (.), id, const) import Data.Functor (Functor(..), (<$>)) import Data.Int (Int) import Data.Kind (Type) import Data.Maybe (Maybe(..), fromMaybe, catMaybes) import Data.Monoid (Monoid(..)) import Data.Ord (Ord(..)) import Data.Proxy (Proxy(..)) import Data.Semigroup (Semigroup(..)) import Data.String (String, IsString(..)) import Data.Text (Text) import System.IO (IO) import Text.Show (Show(..)) import qualified Control.Monad.Classes as MC import qualified Control.Monad.Trans.Cont as C import qualified Control.Monad.Trans.Reader as R import qualified Control.Monad.Trans.State.Strict as S import qualified Control.Monad.Trans.Writer.Strict as W import qualified Data.ByteString as BS import qualified Data.ByteString.Base64 as BS64 import qualified Data.ByteString.Builder as BSB import qualified Data.ByteString.Lazy as BSL import qualified Data.List as List import qualified Data.List.NonEmpty as NonEmpty import qualified Data.Map.Merge.Strict as Map import qualified Data.Map.Strict as Map import qualified Data.Text as Text import qualified Data.Text.Encoding as Text import qualified Data.Word8 as Word8 import qualified Network.HTTP.Media as Media import qualified Network.HTTP.Types as HTTP import qualified Network.HTTP.Types.Header as HTTP import qualified Network.Wai as Wai import qualified Web.HttpApiData as Web import Symantic.HTTP -- * Type 'Server' -- | (@'Server' handlers k@) is a recipe to produce an 'Wai.Application' -- from given ('handlers') (one per number of alternative routes), -- separated by (':!:'). -- -- 'Server' is analogous to a scanf using the API as a format customized for HTTP routing. -- -- The multiple 'ServerCheckT' monad transformers are there -- to prioritize the errors according to the type of check raising them, -- instead of the order of the combinators within an actual API specification. newtype Server handlers k = Server { unServer :: S.StateT ServerState (ServerCheckT [ServerErrorBody] -- 8th check, 400 error (ServerCheckT [ServerErrorHeader] -- 7th check, 400 error (ServerCheckT [ServerErrorQuery] -- 6th check, 400 error (ServerCheckT [ServerErrorContentType] -- 5th check, 415 error (ServerCheckT [ServerErrorAccept] -- 4th check, 406 error (ServerCheckT [ServerErrorBasicAuth] -- 3rd check, 401 or 403 error (ServerCheckT [ServerErrorMethod] -- 2nd check, 405 error (ServerCheckT [ServerErrorPath] -- 1st check, 404 error IO)))))))) (handlers -> k) } -- | (@'server' api handlers@) returns an 'Wai.Application' -- ready to be given to @Warp.run 80@. server :: Router Server handlers (Response Server) -> handlers -> Wai.Application server api handlers rq re = do lrPath <- runServerChecks (unServer $ unTrans $ router api) $ ServerState rq case lrPath of Left err -> respondError HTTP.status404 [] err Right lrMethod -> case lrMethod of Left err -> respondError HTTP.status405 [] err Right lrBasicAuth -> case lrBasicAuth of Left err -> case failError err of [] -> respondError HTTP.status500 [] err ServerErrorBasicAuth realm ba:_ -> case ba of BasicAuth_Unauthorized -> respondError HTTP.status403 [] err _ -> respondError HTTP.status401 [ ( HTTP.hWWWAuthenticate , "Basic realm=\""<>Web.toHeader realm<>"\"" ) ] err Right lrAccept -> case lrAccept of Left err -> respondError HTTP.status406 [] err Right lrContentType -> case lrContentType of Left err -> respondError HTTP.status415 [] err Right lrQuery -> case lrQuery of Left err -> respondError HTTP.status400 [] err Right lrHeader -> case lrHeader of Left err -> respondError HTTP.status400 [] err Right lrBody -> case lrBody of Left err -> respondError HTTP.status400 [] err Right (app, st) -> app handlers (serverState_request st) re where respondError :: Show err => HTTP.Status -> [(HTTP.HeaderName, HeaderValue)] -> err -> IO Wai.ResponseReceived respondError st hs err = -- Trace.trace (show err) $ re $ Wai.responseLBS st ( (HTTP.hContentType, Media.renderHeader $ mediaType @PlainText) : hs ) (fromString $ show err) -- TODO: see what to return in the body -- | Unwrap the 'ExceptT' constructors to reach the 'Either' constructors. runServerChecks :: S.StateT ServerState (ExceptT e1 (ExceptT e2 (ExceptT e3 (ExceptT e4 (ExceptT e5 (ExceptT e6 (ExceptT e7 (ExceptT e8 IO)))))))) a -> ServerState -> IO (Either e8 (Either e7 (Either e6 (Either e5 (Either e4 (Either e3 (Either e2 (Either e1 (a, ServerState))))))))) runServerChecks s st = runExceptT $ runExceptT $ runExceptT $ runExceptT $ runExceptT $ runExceptT $ runExceptT $ runExceptT $ S.runStateT s st -- ** Type 'ServerCheckT' type ServerCheckT e = ExceptT (Fail e) -- *** Type 'RouteResult' type RouteResult e = Either (Fail e) -- *** Type 'Fail' data Fail e = Fail ServerState e -- ^ Keep trying other paths. 404, 405 or 406. | FailFatal !ServerState !e -- ^ Don't try other paths. deriving (Show) failState :: Fail e -> ServerState failState (Fail st _) = st failState (FailFatal st _) = st failError :: Fail e -> e failError (Fail _st e) = e failError (FailFatal _st e) = e instance Semigroup e => Semigroup (Fail e) where Fail _ x <> Fail st y = Fail st (x<>y) FailFatal _ x <> Fail st y = FailFatal st (x<>y) Fail _ x <> FailFatal st y = FailFatal st (x<>y) FailFatal _ x <> FailFatal st y = FailFatal st (x<>y) -- ** Type 'ServerState' newtype ServerState = ServerState { serverState_request :: Wai.Request } -- deriving (Show) instance Show ServerState where show _ = "ServerState" instance Cat Server where (<.>) :: forall a b c repr. repr ~ Server => repr a b -> repr b c -> repr a c -- NOTE: if x fails, run y to see if it fails on a more prioritized check. -- And if so, fail with y instead of x. -- -- This long spaghetti code may probably be avoided -- with a more sophisticated 'Server' using a binary tree -- instead of nested 'Either's, so that its 'Monad' instance -- would do the right thing. But to my mind, -- with the very few priorities of checks currently needed, -- this is not worth the cognitive pain to design it. -- Some copying/pasting/adapting will do for now. Server x <.> Server y = Server $ S.StateT $ \st -> do xPath <- MC.exec @IO $ runServerChecks x st case xPath of Left xe -> MC.throw xe Right xMethod -> case xMethod of Left xe -> do yPath <- MC.exec @IO $ runServerChecks y (failState xe) case yPath of Left ye -> MC.throw ye Right _yMethod -> MC.throw xe Right xBasicAuth -> case xBasicAuth of Left xe -> do yPath <- MC.exec @IO $ runServerChecks y (failState xe) case yPath of Left ye -> MC.throw ye Right yMethod -> case yMethod of Left ye -> MC.throw ye Right _yBasicAuth -> MC.throw xe Right xAccept -> case xAccept of Left xe -> do yPath <- MC.exec @IO $ runServerChecks y (failState xe) case yPath of Left ye -> MC.throw ye Right yMethod -> case yMethod of Left ye -> MC.throw ye Right yBasicAuth -> case yBasicAuth of Left ye -> MC.throw ye Right _yAccept -> MC.throw xe Right xContentType -> case xContentType of Left xe -> do yPath <- MC.exec @IO $ runServerChecks y (failState xe) case yPath of Left ye -> MC.throw ye Right yMethod -> case yMethod of Left ye -> MC.throw ye Right yBasicAuth -> case yBasicAuth of Left ye -> MC.throw ye Right yAccept -> case yAccept of Left ye -> MC.throw ye Right _yQuery -> MC.throw xe Right xQuery -> case xQuery of Left xe -> do yPath <- MC.exec @IO $ runServerChecks y (failState xe) case yPath of Left ye -> MC.throw ye Right yMethod -> case yMethod of Left ye -> MC.throw ye Right yBasicAuth -> case yBasicAuth of Left ye -> MC.throw ye Right yAccept -> case yAccept of Left ye -> MC.throw ye Right yQuery -> case yQuery of Left ye -> MC.throw ye Right _yHeader -> MC.throw xe Right xHeader -> case xHeader of Left xe -> do yPath <- MC.exec @IO $ runServerChecks y (failState xe) case yPath of Left ye -> MC.throw ye Right yMethod -> case yMethod of Left ye -> MC.throw ye Right yBasicAuth -> case yBasicAuth of Left ye -> MC.throw ye Right yAccept -> case yAccept of Left ye -> MC.throw ye Right yQuery -> case yQuery of Left ye -> MC.throw ye Right yHeader -> case yHeader of Left ye -> MC.throw ye Right _yBody -> MC.throw xe Right xBody -> case xBody of Left xe -> do yPath <- MC.exec @IO $ runServerChecks y (failState xe) case yPath of Left ye -> MC.throw ye Right yMethod -> case yMethod of Left ye -> MC.throw ye Right yBasicAuth -> case yBasicAuth of Left ye -> MC.throw ye Right yAccept -> case yAccept of Left ye -> MC.throw ye Right yQuery -> case yQuery of Left ye -> MC.throw ye Right yHeader -> case yHeader of Left ye -> MC.throw ye Right _yBody -> MC.throw xe Right (a2b, st') -> first (. a2b) <$> S.runStateT y st' instance Alt Server where -- () :: repr a k -> repr b k -> repr (a:!:b) k Server x Server y = Server $ S.StateT $ \st -> do xPath <- MC.exec @IO $ runServerChecks x st let fy = (first (\b2k (_a:!:b) -> b2k b) <$>) case xPath of Left xe | FailFatal{} <- xe -> MC.throw xe | otherwise -> do yPath <- MC.exec @IO $ runServerChecks y st case yPath of Left ye -> MC.throw (xe<>ye) Right yMethod -> fy $ ExceptT $ ExceptT $ ExceptT $ ExceptT $ ExceptT $ ExceptT $ ExceptT $ ExceptT $ return $ Right yMethod Right xMethod -> case xMethod of Left xe | FailFatal{} <- xe -> MC.throw xe | otherwise -> do yPath <- MC.exec @IO $ runServerChecks y st case yPath of Left _ye -> MC.throw xe Right yMethod -> case yMethod of Left ye -> MC.throw (xe<>ye) Right yBasicAuth -> fy $ ExceptT $ ExceptT $ ExceptT $ ExceptT $ ExceptT $ ExceptT $ ExceptT $ return $ Right $ yBasicAuth Right xBasicAuth -> case xBasicAuth of Left xe | FailFatal{} <- xe -> MC.throw xe | otherwise -> do yPath <- MC.exec @IO $ runServerChecks y st case yPath of Left _ye -> MC.throw xe Right yMethod -> case yMethod of Left _ye -> MC.throw xe Right yBasicAuth -> case yBasicAuth of Left ye -> MC.throw (xe<>ye) Right yAccept -> fy $ ExceptT $ ExceptT $ ExceptT $ ExceptT $ ExceptT $ ExceptT $ return $ Right yAccept Right xAccept -> case xAccept of Left xe | FailFatal{} <- xe -> MC.throw xe | otherwise -> do yPath <- MC.exec @IO $ runServerChecks y st case yPath of Left _ye -> MC.throw xe Right yMethod -> case yMethod of Left _ye -> MC.throw xe Right yBasicAuth -> case yBasicAuth of Left _ye -> MC.throw xe Right yAccept -> case yAccept of Left ye -> MC.throw (xe<>ye) Right yContentType -> fy $ ExceptT $ ExceptT $ ExceptT $ ExceptT $ ExceptT $ return $ Right yContentType Right xContentType -> case xContentType of Left xe | FailFatal{} <- xe -> MC.throw xe | otherwise -> do yPath <- MC.exec @IO $ runServerChecks y st case yPath of Left _ye -> MC.throw xe Right yMethod -> case yMethod of Left _ye -> MC.throw xe Right yBasicAuth -> case yBasicAuth of Left _ye -> MC.throw xe Right yAccept -> case yAccept of Left _ye -> MC.throw xe Right yContentType -> case yContentType of Left ye -> MC.throw (xe<>ye) Right yQuery -> fy $ ExceptT $ ExceptT $ ExceptT $ ExceptT $ return $ Right yQuery Right xQuery -> case xQuery of Left xe | FailFatal{} <- xe -> MC.throw xe | otherwise -> do yPath <- MC.exec @IO $ runServerChecks y st case yPath of Left _ye -> MC.throw xe Right yMethod -> case yMethod of Left _ye -> MC.throw xe Right yBasicAuth -> case yBasicAuth of Left _ye -> MC.throw xe Right yAccept -> case yAccept of Left _ye -> MC.throw xe Right yContentType -> case yContentType of Left _ye -> MC.throw xe Right yQuery -> case yQuery of Left ye -> MC.throw (xe<>ye) Right yHeader -> fy $ ExceptT $ ExceptT $ ExceptT $ return $ Right yHeader Right xHeader -> case xHeader of Left xe | FailFatal{} <- xe -> MC.throw xe | otherwise -> do yPath <- MC.exec @IO $ runServerChecks y st case yPath of Left _ye -> MC.throw xe Right yMethod -> case yMethod of Left _ye -> MC.throw xe Right yBasicAuth -> case yBasicAuth of Left _ye -> MC.throw xe Right yAccept -> case yAccept of Left _ye -> MC.throw xe Right yContentType -> case yContentType of Left _ye -> MC.throw xe Right yQuery -> case yQuery of Left _ye -> MC.throw xe Right yHeader -> case yHeader of Left ye -> MC.throw (xe<>ye) Right yBody -> fy $ ExceptT $ ExceptT $ return $ Right yBody Right xBody -> case xBody of Left xe | FailFatal{} <- xe -> MC.throw xe | otherwise -> do yPath <- MC.exec @IO $ runServerChecks y st case yPath of Left _ye -> MC.throw xe Right yMethod -> case yMethod of Left _ye -> MC.throw xe Right yBasicAuth -> case yBasicAuth of Left _ye -> MC.throw xe Right yAccept -> case yAccept of Left _ye -> MC.throw xe Right yContentType -> case yContentType of Left _ye -> MC.throw xe Right yQuery -> case yQuery of Left _ye -> MC.throw xe Right yHeader -> case yHeader of Left _ye -> MC.throw xe Right yBody -> case yBody of Left ye -> MC.throw (xe<>ye) Right yr -> fy $ ExceptT $ return $ Right yr Right xr -> return $ first (\a2k (a:!:_b) -> a2k a) xr instance Pro Server where dimap a2b _b2a (Server r) = Server $ (\k b2k -> k (b2k . a2b)) <$> r -- ** Type 'ServerErrorPath' newtype ServerErrorPath = ServerErrorPath Text deriving (Eq, Show) instance HTTP_Path Server where type PathConstraint Server a = Web.FromHttpApiData a segment expSegment = Server $ do st@ServerState { serverState_request = req } <- S.get case Wai.pathInfo req of [] -> MC.throw $ Fail st [ServerErrorPath "empty path segment"] [""] -> MC.throw $ Fail st [ServerErrorPath "trailing slash"] curr:next | curr /= expSegment -> MC.throw $ Fail st [ServerErrorPath $ "expected: "<>expSegment<>" but got: "<>curr] | otherwise -> do S.put st { serverState_request = req{ Wai.pathInfo = next } } return id capture' :: forall a k. PathConstraint Server a => Name -> Server (a -> k) k capture' name = Server $ do st@ServerState { serverState_request = req } <- S.get case Wai.pathInfo req of [] -> MC.throw $ Fail st [ServerErrorPath "empty"] [""] -> MC.throw $ Fail st [ServerErrorPath "trailing slash"] curr:next -> case Web.parseUrlPiece curr of Left err -> MC.throw $ Fail st [ServerErrorPath $ "capture: "<>fromString name<>": "<>err] Right a -> do S.put st { serverState_request = req{ Wai.pathInfo = next } } return ($ a) captureAll = Server $ do req <- S.gets serverState_request return ($ Wai.pathInfo req) -- ** Type 'ServerErrorMethod' data ServerErrorMethod = ServerErrorMethod deriving (Eq, Show) -- | TODO: add its own error? instance HTTP_Version Server where version exp = Server $ do st <- S.get let got = Wai.httpVersion $ serverState_request st if got == exp then return id else MC.throw $ Fail st [ServerErrorMethod] -- FIXME: ServerErrorVersion -- ** Type 'ServerErrorAccept' data ServerErrorAccept = ServerErrorAccept MediaTypes (Maybe (Either BS.ByteString MediaType)) deriving (Eq, Show) -- ** Type 'ServerErrorContentType' data ServerErrorContentType = ServerErrorContentType deriving (Eq, Show) -- ** Type 'ServerErrorQuery' newtype ServerErrorQuery = ServerErrorQuery Text deriving (Show) instance HTTP_Query Server where type QueryConstraint Server a = Web.FromHttpApiData a queryParams' name = Server $ do st <- S.get lift $ ExceptT $ ExceptT $ ExceptT $ return $ let qs = Wai.queryString $ serverState_request st in let vals = catMaybes $ (<$> qs) $ \(n,v) -> if n == name then Web.parseQueryParam . Text.decodeUtf8 <$> v else Nothing in case sequence vals of Left err -> Left $ Fail st [ServerErrorQuery err] Right vs -> Right $ Right $ Right ($ vs) -- ** Type 'ServerErrorHeader' data ServerErrorHeader = ServerErrorHeader deriving (Eq, Show) instance HTTP_Header Server where header n = Server $ do st <- S.get lift $ ExceptT $ ExceptT $ return $ let hs = Wai.requestHeaders $ serverState_request st in case List.lookup n hs of Nothing -> Left $ Fail st [ServerErrorHeader] Just v -> Right $ Right ($ v) instance HTTP_Raw Server where type RawConstraint Server = () type RawArgs Server = Wai.Application type Raw Server = Wai.Application raw = Server $ return id -- ** Type 'ServerErrorBasicAuth' data ServerErrorBasicAuth = ServerErrorBasicAuth BasicAuthRealm (BasicAuth ()) deriving (Show) -- ** Class 'ServerBasicAuth' -- | Custom 'BasicAuth' check. class ServerBasicAuth a where serverBasicAuth :: BasicAuthUser -> BasicAuthPass -> IO (BasicAuth a) -- | WARNING: current implementation of Basic Access Authentication -- is not immune to certain kinds of timing attacks. -- Decoding payloads does not take a fixed amount of time. instance HTTP_BasicAuth Server where type BasicAuthConstraint Server a = ServerBasicAuth a type BasicAuthArgs Server a k = a -> k basicAuth' realm = Server $ do st <- S.get let err e = MC.throw $ Fail st [ServerErrorBasicAuth realm e] case decodeAuthorization $ serverState_request st of Nothing -> err BasicAuth_BadPassword Just (user, pass) -> do MC.exec @IO (serverBasicAuth user pass) >>= \case BasicAuth_BadPassword -> err BasicAuth_BadPassword BasicAuth_NoSuchUser -> err BasicAuth_NoSuchUser BasicAuth_Unauthorized -> err BasicAuth_Unauthorized BasicAuth_Authorized u -> return ($ u) where -- | Find and decode an 'Authorization' header from the request as a Basic Auth decodeAuthorization :: Wai.Request -> Maybe (BasicAuthUser, BasicAuthPass) decodeAuthorization req = do hAuthorization <- List.lookup "Authorization" $ Wai.requestHeaders req let (basic, rest) = BS.break Word8.isSpace hAuthorization guard (BS.map Word8.toLower basic == "basic") let decoded = BS64.decodeLenient (BS.dropWhile Word8.isSpace rest) let (user, colon_pass) = BS.break (== Word8._colon) decoded (_, pass) <- BS.uncons colon_pass return (Text.decodeUtf8 user, Text.decodeUtf8 pass) -- ** Type 'ServerErrorBody' newtype ServerErrorBody = ServerErrorBody String deriving (Eq, Show) -- *** Type 'ServerBodyArg' newtype ServerBodyArg (ts::[Type]) a = ServerBodyArg a instance HTTP_Body Server where type BodyConstraint Server a ts = MimeTypes ts (MimeDecodable a) type BodyArg Server a ts = ServerBodyArg ts a body' :: forall a ts k repr. BodyConstraint repr a ts => repr ~ Server => repr (BodyArg repr a ts -> k) k body'= Server $ do st <- S.get lift $ ExceptT $ ExceptT $ ExceptT $ ExceptT $ do let hs = Wai.requestHeaders $ serverState_request st let reqContentType = -- DOC: http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1 -- DOC: http://www.w3.org/2001/tag/2002/0129-mime fromMaybe "application/octet-stream" $ List.lookup HTTP.hContentType hs case matchContent @ts @(MimeDecodable a) reqContentType of Nothing -> return $ Left $ Fail st [ServerErrorContentType] Just (MimeType mt) -> do bodyBS <- MC.exec @IO $ Wai.requestBody $ serverState_request st return $ Right $ Right $ Right $ -- NOTE: delay 'mimeDecode' after all checks. case mimeDecode mt $ BSL.fromStrict bodyBS of Left err -> Left $ Fail st [ServerErrorBody err] Right a -> Right ($ ServerBodyArg a) -- *** Type 'ServerBodyStreamArg' newtype ServerBodyStreamArg as (ts::[Type]) framing = ServerBodyStreamArg as instance HTTP_BodyStream Server where type BodyStreamConstraint Server as ts framing = ( FramingDecode framing as , MC.MonadExec IO (FramingMonad as) , MimeTypes ts (MimeDecodable (FramingYield as)) ) type BodyStreamArg Server as ts framing = ServerBodyStreamArg as ts framing bodyStream' :: forall as ts framing k repr. BodyStreamConstraint repr as ts framing => repr ~ Server => repr (BodyStreamArg repr as ts framing -> k) k bodyStream'= Server $ do st <- S.get lift $ ExceptT $ ExceptT $ ExceptT $ ExceptT $ do let hs = Wai.requestHeaders $ serverState_request st let reqContentType = -- DOC: http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1 -- DOC: http://www.w3.org/2001/tag/2002/0129-mime fromMaybe "application/octet-stream" $ List.lookup HTTP.hContentType hs case matchContent @ts @(MimeDecodable (FramingYield as)) reqContentType of Nothing -> return $ Left $ Fail st [ServerErrorContentType] Just (MimeType mt) -> do let bodyBS = Wai.requestBody $ serverState_request st return $ Right $ Right $ Right $ Right ($ ServerBodyStreamArg $ framingDecode (Proxy @framing) (mimeDecode mt) $ MC.exec @IO bodyBS ) -- * Type 'ServerResponse' -- | A continuation for 'server''s users to respond. -- -- This newtype has two usesĀ : -- -- * Carrying the 'ts' type variable to 'server'. -- * Providing a 'return' for the simple response case -- of 'HTTP.status200' and no extra headers. newtype ServerRes (ts::[Type]) m a = ServerResponse { unServerResponse :: m a } deriving (Functor, Applicative, Monad) type ServerResponse ts m = ServerRes ts (R.ReaderT Wai.Request (W.WriterT HTTP.ResponseHeaders (W.WriterT HTTP.Status (C.ContT Wai.Response m)))) instance MonadTrans (ServerRes ts) where lift = ServerResponse -- | All supported effects are handled by nested 'Monad's. type instance MC.CanDo (ServerResponse ts m) eff = 'False type instance MC.CanDo (C.ContT ts m) (MC.EffExec eff) = 'False instance HTTP_Response Server where type ResponseConstraint Server a ts = MimeTypes ts (MimeEncodable a) type ResponseArgs Server a ts = ServerResponse ts IO a type Response Server = Wai.Request -> (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived response :: forall a ts repr. ResponseConstraint repr a ts => repr ~ Server => HTTP.Method -> repr (ResponseArgs repr a ts) (Response repr) response expMethod = Server $ do st@ServerState { serverState_request = req } <- S.get -- Check the path has been fully consumed unless (List.null $ Wai.pathInfo req) $ MC.throw $ Fail st [ServerErrorPath "path is longer"] -- Check the method let reqMethod = Wai.requestMethod $ serverState_request st unless (reqMethod == expMethod || reqMethod == HTTP.methodHead && expMethod == HTTP.methodGet) $ MC.throw $ Fail st [ServerErrorMethod] -- Check the Accept header let reqHeaders = Wai.requestHeaders $ serverState_request st MimeType reqAccept <- do case List.lookup HTTP.hAccept reqHeaders of Nothing -> return $ NonEmpty.head $ mimeTypes @ts @(MimeEncodable a) Just h -> case matchAccept @ts @(MimeEncodable a) h of Nothing -> MC.throw $ Fail st [ServerErrorAccept (mediaTypes @ts @(MimeEncodable a)) (Just (Left h))] Just mt -> return mt return $ \(ServerResponse k) rq re -> re =<< do C.runContT (W.runWriterT $ W.runWriterT $ R.runReaderT k rq) $ \((a,hs),sta) -> return{-IO-} $ Wai.responseLBS sta ((HTTP.hContentType, Media.renderHeader $ mediaTypeFor reqAccept):hs) (if reqMethod == HTTP.methodHead then "" else mimeEncode reqAccept a) -- * Type 'ServerResponseStream' -- -- This newtype has three usesĀ : -- -- * Carrying the 'framing' type variable to 'server'. -- * Carrying the 'ts' type variable to 'server'. -- * Providing a 'return' for the simple response case -- of 'HTTP.status200' and no extra headers. newtype ServerResStream framing (ts::[Type]) m as = ServerResponseStream { unServerResponseStream :: m as } deriving (Functor, Applicative, Monad) instance MonadTrans (ServerResStream framing ts) where lift = ServerResponseStream type ServerResponseStream framing ts m = ServerResStream framing ts (R.ReaderT Wai.Request (W.WriterT HTTP.ResponseHeaders (W.WriterT HTTP.Status (C.ContT Wai.Response m)))) -- | All supported effects are handled by nested 'Monad's. type instance MC.CanDo (ServerResponseStream framing ts m) eff = 'False instance HTTP_ResponseStream Server where type ResponseStreamConstraint Server as ts framing = ( FramingEncode framing as , MimeTypes ts (MimeEncodable (FramingYield as)) ) type ResponseStreamArgs Server as ts framing = ServerResponseStream framing ts IO as type ResponseStream Server = Wai.Application {- Wai.Request -> (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived -} responseStream :: forall as ts framing repr. ResponseStreamConstraint repr as ts framing => repr ~ Server => HTTP.Method -> repr (ResponseStreamArgs repr as ts framing) (ResponseStream repr) responseStream expMethod = Server $ do st@ServerState { serverState_request = req } <- S.get -- Check the path has been fully consumed unless (List.null $ Wai.pathInfo req) $ MC.throw $ Fail st [ServerErrorPath "path is longer"] -- Check the method let reqMethod = Wai.requestMethod $ serverState_request st unless (reqMethod == expMethod || reqMethod == HTTP.methodHead && expMethod == HTTP.methodGet) $ MC.throw $ Fail st [ServerErrorMethod] -- Check the Accept header let reqHeaders = Wai.requestHeaders $ serverState_request st MimeType reqAccept <- do case List.lookup HTTP.hAccept reqHeaders of Nothing -> return $ NonEmpty.head $ mimeTypes @ts @(MimeEncodable (FramingYield as)) Just h -> case matchAccept @ts @(MimeEncodable (FramingYield as)) h of Nothing -> MC.throw $ Fail st [ServerErrorAccept (mediaTypes @ts @(MimeEncodable (FramingYield as))) (Just (Left h))] Just mt -> return mt return $ \(ServerResponseStream k) rq re -> re =<< do C.runContT (W.runWriterT $ W.runWriterT $ R.runReaderT k rq) $ \((as,hs),sta) -> return{-IO-} $ Wai.responseStream sta ( (HTTP.hContentType, Media.renderHeader $ mediaTypeFor reqAccept) : hs ) $ \write flush -> if reqMethod == HTTP.methodHead then flush else let enc = framingEncode (Proxy @framing) (mimeEncode reqAccept) in let go curr = case curr of Left _end -> flush Right (bsl, next) -> do unless (BSL.null bsl) $ do write (BSB.lazyByteString bsl) flush enc next >>= go in enc as >>= go -- | Return worse 'HTTP.Status'. instance Semigroup HTTP.Status where x <> y = if rank (HTTP.statusCode x) < rank (HTTP.statusCode y) then x else y where rank :: Int -> Int rank 404 = 0 -- Not Found rank 405 = 1 -- Method Not Allowed rank 401 = 2 -- Unauthorized rank 415 = 3 -- Unsupported Media Type rank 406 = 4 -- Not Acceptable rank 400 = 5 -- Bad Request rank _ = 6 -- | Useful when 'HTTP.Status' is within a 'W.WriterT'. instance Monoid HTTP.Status where mempty = HTTP.status200 mappend = (<>) -- * Type 'Router' -- | 'Trans'form a 'Server' to merge 'Alt'ernative 'segment's into a 'routing'. data Router repr a b where -- | Lift any @(repr)@ into 'Router', those not useful to segregate -- wrt. the 'Trans'formation performed, aka. 'noTrans'. Router_Any :: repr a b -> Router repr a b -- | Represent 'segment'. Router_Seg :: PathSegment -> Router repr k k -- | Represent ('<.>'). Router_Cat :: Router repr a b -> Router repr b c -> Router repr a c -- | Represent 'routing'. Router_Map :: Map.Map PathSegment (Router repr a k) -> Router repr a k -- | Represent (''). Router_Alt :: Router repr a k -> Router repr b k -> Router repr (a:!:b) k -- | Represent 'capture''. Router_Cap :: PathConstraint Server a => Name -> Router repr (a->k) k -- | Represent 'captures'. Router_Caps :: Captures (Router repr) cs k -> Router repr (AltFromBinTree cs) k -- | Unify 'Router's which have different 'handlers'. -- Useful to put alternative 'Router's in a 'Map.Map' as in 'Router_Map'. Router_Union :: (b->a) -> Router repr a k -> Router repr b k -- ** Type 'Captures' data Captures repr (cs::BinTree Type) k where Captures0 :: PathConstraint Server a => Proxy a -> Name -> repr x k -> Captures repr ('BinTree0 (a->x)) k Captures2 :: Captures repr x k -> Captures repr y k -> Captures repr ('BinTree2 x y) k -- *** Type 'BinTree' -- | Use @DataKinds@ to define a 'BinTree' of 'Type's. -- Useful for gathering together 'capture's of different 'Type's. data BinTree a = BinTree0 a | BinTree2 (BinTree a) (BinTree a) -- *** Type family 'AltFromBinTree' type family AltFromBinTree (cs::BinTree Type) :: Type where AltFromBinTree ('BinTree0 x) = x AltFromBinTree ('BinTree2 x y) = AltFromBinTree x :!: AltFromBinTree y instance Trans (Router Server) where type UnTrans (Router Server) = Server noTrans = Router_Any unTrans (Router_Any x) = x unTrans (Router_Seg s) = segment s unTrans (Router_Cat x y) = unTrans x <.> unTrans y unTrans (Router_Alt x y) = unTrans x unTrans y unTrans (Router_Map ms) = routing (unTrans <$> ms) unTrans (Router_Cap n) = capture' n unTrans (Router_Caps xs) = captures $ unTransCaptures xs where unTransCaptures :: Captures (Router Server) cs k -> Captures Server cs k unTransCaptures (Captures0 a n r) = Captures0 a n (unTrans r) unTransCaptures (Captures2 x y) = unTransCaptures x `Captures2` unTransCaptures y unTrans (Router_Union u x) = Server $ (. u) <$> unServer (unTrans x) instance Cat (Router Server) where (<.>) = Router_Cat instance Alt (Router Server) where () = Router_Alt instance repr ~ Server => HTTP_Path (Router repr) where type PathConstraint (Router repr) a = PathConstraint repr a segment = Router_Seg capture' = Router_Cap instance HTTP_Routing (Router Server) where routing = Router_Map captures = Router_Caps instance HTTP_Raw (Router Server) instance Pro (Router Server) instance HTTP_Query (Router Server) instance HTTP_Header (Router Server) instance HTTP_Body (Router Server) instance HTTP_BodyStream (Router Server) instance HTTP_BasicAuth (Router Server) instance HTTP_Response (Router Server) instance HTTP_ResponseStream (Router Server) -- ** Class 'HTTP_Routing' class HTTP_Routing repr where routing :: Map.Map PathSegment (repr a k) -> repr a k captures :: Captures repr cs k -> repr (AltFromBinTree cs) k -- Trans defaults default routing :: Trans repr => HTTP_Routing (UnTrans repr) => Map.Map PathSegment (repr a k) -> repr a k routing = noTrans . routing . (unTrans <$>) default captures :: Trans repr => HTTP_Routing (UnTrans repr) => Captures repr cs k -> repr (AltFromBinTree cs) k captures = noTrans . captures . unTransCaptures where unTransCaptures :: Captures repr cs k -> Captures (UnTrans repr) cs k unTransCaptures (Captures0 a n r) = Captures0 a n (unTrans r) unTransCaptures (Captures2 x y) = Captures2 (unTransCaptures x) (unTransCaptures y) instance HTTP_Routing Server where routing ms = Server $ do st@ServerState { serverState_request = req } <- S.get case Wai.pathInfo req of [] -> MC.throw $ Fail st [ServerErrorPath "empty path segment"] [""] -> MC.throw $ Fail st [ServerErrorPath "trailing slash"] curr:next -> case Map.lookup curr ms of Nothing -> MC.throw $ Fail st [ServerErrorPath $ "expected: "<>Text.pack (show (Map.keys ms))<>" but got: "<>curr] Just x -> do S.put st { serverState_request = req{ Wai.pathInfo = next } } unServer x captures :: Captures Server cs k -> Server (AltFromBinTree cs) k captures cs = Server $ do st@ServerState { serverState_request = req } <- S.get case Wai.pathInfo req of [] -> MC.throw $ Fail st [ServerErrorPath "empty"] [""] -> MC.throw $ Fail st [ServerErrorPath "trailing slash"] currSeg:nextSeg -> case go cs of Left errs -> MC.throw $ Fail st [ServerErrorPath $ "captures: "<> fromString (List.intercalate "|" ((\(name,err) -> name) <$> errs))] Right a -> unServer a where go :: forall cs k. Captures Server cs k -> Either [(Name,Text)] (Server (AltFromBinTree cs) k) go (Captures0 (Proxy::Proxy a) name currRepr) = case Web.parseUrlPiece currSeg of Left err -> Left [(name,err)] Right (a::a) -> Right $ Server $ do S.put st { serverState_request = req{ Wai.pathInfo = nextSeg } } (\x2k a2x -> x2k (a2x a)) <$> unServer currRepr go (Captures2 x y) = case go x of Left xe -> case go y of Left ye -> Left (xe<>ye) Right a -> Right $ Server $ (\r2k (_l:!:r) -> r2k r) <$> unServer a Right a -> Right $ Server $ (\l2k (l:!:_r) -> l2k l) <$> unServer a -- | Traverse a 'Router' to transform it: -- -- * Associate 'Router_Cat' to the right. -- * Replace 'Router_Seg' with 'Router_Map'. -- * Replace 'Router_Cap' with 'Router_Caps'. -- -- Used in 'server' on the 'Router' inferred from the given API. router :: Router repr a b -> Router repr a b router = {-debug1 "router" $-} \case x@Router_Any{} -> x x@Router_Seg{} -> x Router_Seg x `Router_Cat` y -> Router_Map $ Map.singleton x $ router y Router_Alt x y -> x`router_Alt`y Router_Map xs -> Router_Map $ router <$> xs Router_Cap xn `Router_Cat` x -> Router_Caps $ Captures0 Proxy xn x Router_Cap n -> Router_Cap n Router_Caps cs -> Router_Caps (go cs) where go :: Captures (Router repr) cs k -> Captures (Router repr) cs k go (Captures0 a n r) = Captures0 a n (router r) go (Captures2 x y) = Captures2 (go x) (go y) Router_Cat xy z -> case xy of Router_Cat x y -> -- Associate to the right Router_Cat (router x) $ Router_Cat (router y) (router z) _ -> router xy `Router_Cat` router z Router_Union u x -> Router_Union u (router x) -- | Merge/reorder alternatives if possible or default to a 'Router_Alt'. router_Alt :: Router repr a k -> Router repr b k -> Router repr (a:!:b) k router_Alt = {-debug2 "router_Alt"-} go where -- Merge alternative segments together. go (Router_Seg x `Router_Cat` xt) (Router_Seg y `Router_Cat` yt) = Map.singleton x (router xt) `router_Map` Map.singleton y (router yt) go (Router_Seg x `Router_Cat` xt) (Router_Map ys) = Map.singleton x (router xt) `router_Map` ys go (Router_Map xs) (Router_Seg y `Router_Cat` yt) = xs `router_Map` Map.singleton y (router yt) go (Router_Map xs) (Router_Map ys) = xs`router_Map`ys -- Merge alternative 'segment's or alternative 'capture''s together. go (Router_Cap xn `Router_Cat` x) (Router_Cap yn `Router_Cat` y) = Router_Caps $ Captures0 Proxy xn x `Captures2` Captures0 Proxy yn y go (Router_Caps xs) (Router_Caps ys) = Router_Caps $ xs`Captures2`ys go (Router_Cap xn `Router_Cat` x) (Router_Caps ys) = Router_Caps $ Captures0 Proxy xn x `Captures2` ys go (Router_Caps xs) (Router_Cap yn `Router_Cat` y) = Router_Caps $ xs `Captures2` Captures0 Proxy yn y -- Merge left first or right first, depending on which removes 'Router_Alt'. go x (y`Router_Alt`z) = case x`router_Alt`y of Router_Alt x' y' -> case y'`router_Alt`z of yz@(Router_Alt _y z') -> case x'`router_Alt`z' of Router_Alt{} -> router x'`Router_Alt`yz xz -> Router_Union (\(a:!:(b:!:c)) -> (a:!:c):!:b) $ xz`router_Alt`y -- NOTE: prioritize the merged router 'xz' over over the non-mergeable 'y'. yz -> x'`router_Alt`yz xy -> Router_Union (\(a:!:(b:!:c)) -> (a:!:b):!:c) $ xy`router_Alt`z go (x`Router_Alt`y) z = case y`router_Alt`z of Router_Alt y' z' -> case x`router_Alt`y' of xy@(Router_Alt x' _y) -> case x'`router_Alt`z' of Router_Alt{} -> xy`Router_Alt`router z' xz -> Router_Union (\((a:!:b):!:c) -> (a:!:c):!:b) $ xz`router_Alt`y -- NOTE: prioritize the merged router 'xz' over the non-mergeable 'y'. xy -> xy`router_Alt`z' yz -> Router_Union (\((a:!:b):!:c) -> a:!:(b:!:c)) $ x`router_Alt`yz -- Merge through 'Router_Union'. go (Router_Union u x) y = Router_Union (\(a:!:b) -> u a:!:b) (x`router_Alt`y) go x (Router_Union u y) = Router_Union (\(a:!:b) -> a:!:u b) (x`router_Alt`y) -- No merging, but apply 'router' on both alternatives. go x y = router x `Router_Alt` router y router_Map :: Map.Map PathSegment (Router repr a k) -> Map.Map PathSegment (Router repr b k) -> Router repr (a:!:b) k router_Map xs ys = -- NOTE: a little bit more complex than required -- in order to merge 'Router_Union's instead of stacking them, -- such that 'unTrans' 'Router_Union' applies them all at once. Router_Map $ Map.merge (Map.traverseMissing $ const $ \case Router_Union u r -> return $ Router_Union (\(x:!:_y) -> u x) r r -> return $ Router_Union (\(x:!:_y) -> x) r) (Map.traverseMissing $ const $ \case Router_Union u r -> return $ Router_Union (\(_x:!:y) -> u y) r r -> return $ Router_Union (\(_x:!:y) -> y) r) (Map.zipWithAMatched $ const $ \case Router_Union xu xr -> \case Router_Union yu yr -> return $ Router_Union (\(x:!:y) -> xu x:!:yu y) $ xr`router_Alt`yr yr -> return $ Router_Union (\(a:!:b) -> xu a:!:b) $ xr`router_Alt`yr xr -> \case Router_Union yu yr -> return $ Router_Union (\(a:!:b) -> a:!:yu b) $ xr`router_Alt`yr yr -> return $ xr`router_Alt`yr) xs ys {- debug0 :: Show a => String -> a -> a debug0 n a = Debug.trace (" {"<>n<>": "<>show a) a debug1 :: Show a => Show b => String -> (a->b) -> (a->b) debug1 n a2b a = Debug.trace ("} "<>n<>": r: "<>show b) b where b = a2b $ Debug.trace ("{ "<>n<>": a: "<>show a) a debug2 :: Show a => Show b => Show c => String -> (a->b->c) -> (a->b->c) debug2 n a2b2c a b = Debug.trace ("} "<>n<>": r: "<>show c) c where b2c = a2b2c $ Debug.trace ("{ "<>n<>": a: "<>show a) a c = b2c $ Debug.trace (n<>": b: "<>show b) b -}