#if __GLASGOW_HASKELL__ >= 800
#endif
module WebApi.Internal where
import Blaze.ByteString.Builder (Builder, toByteString)
import qualified Blaze.ByteString.Builder.Char.Utf8 as Utf8 (fromText)
import Control.Exception
import Control.Monad.Catch (MonadCatch)
import Control.Monad.IO.Class (MonadIO)
import Control.Monad.Trans.Resource (runResourceT,
withInternalState)
import Data.ByteString (ByteString)
import Data.ByteString.Char8 (pack, unpack)
import Data.List (find, foldl')
import Data.Monoid ((<>))
import Data.Maybe (fromMaybe)
import Data.Proxy
import Data.Text (Text)
import qualified Data.Text as T (pack)
import Data.Text.Encoding (decodeUtf8)
import Data.Typeable (Typeable)
import Network.HTTP.Media (MediaType, mapAcceptMedia,
matchAccept)
import Network.HTTP.Media.RenderHeader (renderHeader)
import Network.HTTP.Types hiding (Query)
import Network.URI (URI (..))
import qualified Network.Wai as Wai
import qualified Network.Wai.Parse as Wai
import Web.Cookie
import WebApi.ContentTypes
import WebApi.Contract
import WebApi.Param
import WebApi.Util
data RouteResult a = NotMatched | Matched a
type RoutingApplication = Wai.Request -> (RouteResult Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived
toApplication :: RoutingApplication -> Wai.Application
toApplication app request respond =
app request $ \routeResult -> case routeResult of
Matched result -> respond result
NotMatched -> respond (Wai.responseLBS notFound404 [] "")
fromWaiRequest :: forall m r.
( FromParam (QueryParam m r) 'QueryParam
, FromParam (FormParam m r) 'FormParam
, FromParam (FileParam m r) 'FileParam
, FromHeader (HeaderIn m r)
, FromParam (CookieIn m r) 'Cookie
, ToHListRecTuple (StripContents (RequestBody m r))
, PartDecodings (RequestBody m r)
, SingMethod m
) => Wai.Request
-> PathParam m r
-> IO (Validation [ParamErr] (Request m r))
fromWaiRequest waiReq pathPar = do
let mContentTy = getContentType $ Wai.requestHeaders waiReq
(formPar, filePar, rBody) <- case hasFormData mContentTy of
Just _ -> do
(formPar, filePar) <- runResourceT $ withInternalState $
\internalState -> Wai.parseRequestBody (Wai.tempFileBackEnd internalState) waiReq
print $ hasFormData $ getContentType $ Wai.requestHeaders waiReq
return (formPar, filePar, [])
Nothing -> do
bdy <- Wai.requestBody waiReq
print bdy
return ([], [], [(fromMaybe (renderHeader $ contentType (Proxy :: Proxy OctetStream)) mContentTy, bdy)])
return $ Request <$> pure pathPar
<*> (fromQueryParam $ Wai.queryString waiReq)
<*> (fromFormParam formPar)
<*> (fromFileParam filePar)
<*> (fromHeader $ Wai.requestHeaders waiReq)
<*> (fromCookie $ maybe [] parseCookies (getCookie waiReq))
<*> (fromBody rBody)
where
hasFormData x = matchAccept [contentType (Proxy :: Proxy MultipartFormData), contentType (Proxy :: Proxy UrlEncoded)] =<< x
fromBody x = Validation $ either (const (Left [NotFound "415"])) (Right . fromRecTuple (Proxy :: Proxy (StripContents (RequestBody m r)))) $ partDecodings (Proxy :: Proxy (RequestBody m r)) x
toWaiResponse :: ( ToHeader (HeaderOut m r)
, ToParam (CookieOut m r) 'Cookie
, Encodings (ContentTypes m r) (ApiOut m r)
, Encodings (ContentTypes m r) (ApiErr m r)
) => Wai.Request -> Response m r -> Wai.Response
toWaiResponse wreq resp = case resp of
Success status out hdrs cookies -> case encode' resp out of
Just (ctype, o') -> let hds = (hContentType, renderHeader ctype) : handleHeaders' (toHeader hdrs) (toCookie cookies)
in Wai.responseBuilder status hds o'
Nothing -> Wai.responseBuilder notAcceptable406 [] "Matching content type not found"
Failure (Left (ApiError status errs hdrs cookies)) -> case encode' resp errs of
Just (ctype, errs') -> let hds = (hContentType, renderHeader ctype) : handleHeaders (toHeader <$> hdrs) (toCookie <$> cookies)
in Wai.responseBuilder status hds errs'
Nothing -> Wai.responseBuilder notAcceptable406 [] "Matching content type not found"
Failure (Right (OtherError ex)) -> Wai.responseBuilder internalServerError500 [] (Utf8.fromText (T.pack (displayException ex)))
where encode' :: ( Encodings (ContentTypes m r) a
) => apiRes m r -> a -> Maybe (MediaType, Builder)
encode' r o = case getAccept wreq of
Just acc -> let ecs = encodings (reproxy r) o
in (,) <$> matchAccept (map fst ecs) acc <*> mapAcceptMedia ecs acc
Nothing -> case encodings (reproxy r) o of
(x : _) -> Just x
_ -> Nothing
reproxy :: apiRes m r -> Proxy (ContentTypes m r)
reproxy = const Proxy
handleHeaders :: Maybe [Header] -> Maybe [(ByteString, ByteString)] -> [Header]
handleHeaders hds cks = handleHeaders' (maybe [] id hds) (maybe [] id cks)
handleHeaders' :: [Header] -> [(ByteString, ByteString)] -> [Header]
handleHeaders' hds cookies = let ckHs = map (\(ck, cv) -> (hSetCookie , renderSC ck cv)) cookies
in hds <> ckHs
renderSC k v = toByteString (renderSetCookie (def { setCookieName = k, setCookieValue = v }))
link :: ( ToParam (QueryParam m r) 'QueryParam
, MkPathFormatString r
, ToParam (PathParam m r) 'PathParam
) =>
route m r
-> URI
-> PathParam m r
-> Maybe (QueryParam m r)
-> URI
link r base paths query = base
{ uriPath = unpack $ renderUriPath (pack $ uriPath base) paths r
, uriQuery = maybe "" renderQuery' query
}
where renderQuery' :: (ToParam query 'QueryParam) => query -> String
renderQuery' = unpack . renderQuery True . toQueryParam
renderUriPath :: ( ToParam path 'PathParam
, MkPathFormatString r
) => ByteString -> path -> route m r -> ByteString
renderUriPath basePth p r = case basePth of
"" -> renderPaths p r
"/" -> renderPaths p r
_ -> basePth `mappend` renderPaths p r
renderPaths :: ( ToParam path 'PathParam
, MkPathFormatString r
) => path -> route m r -> ByteString
renderPaths p r = toByteString
$ encodePathSegments $ uriPathPieces (toPathParam p)
where uriPathPieces :: [ByteString] -> [Text]
uriPathPieces dynVs = reverse $ fst $ foldl' (flip fillHoles) ([], dynVs) (mkPathFormatString (toRoute r))
fillHoles :: PathSegment -> ([Text], [ByteString]) -> ([Text], [ByteString])
fillHoles (StaticSegment t) (segs, dynVs) = (t : segs, dynVs)
fillHoles Hole (segs, dynV: xs) = (decodeUtf8 dynV : segs, xs)
fillHoles Hole (_segs, []) = error "Panic: fewer pathparams than holes"
toRoute :: route m r -> Proxy r
toRoute = const Proxy
class (ApiContract (ApiInterface p) m r) => ApiHandler (p :: *) (m :: *) (r :: *) where
handler :: (query ~ '[])
=> Tagged query p
-> Request m r
-> HandlerM p (Query (Response m r) query)
type family Query (t :: *) (query :: [*]) :: * where
Query t '[] = t
class ( MonadCatch (HandlerM p)
, MonadIO (HandlerM p)
, WebApi (ApiInterface p)
) => WebApiImplementation (p :: *) where
type HandlerM p :: * -> *
type ApiInterface p :: *
toIO :: p -> HandlerM p a -> IO a
default toIO :: (HandlerM p ~ IO) => p -> HandlerM p a -> IO a
toIO _ = id
type HandlerM p = IO
data ServerSettings = ServerSettings
serverSettings :: ServerSettings
serverSettings = ServerSettings
data PathSegment = StaticSegment Text
| Hole
deriving (Show, Eq)
class MkPathFormatString r where
mkPathFormatString :: Proxy r -> [PathSegment]
data ApiException m r = ApiException { apiException :: ApiError m r }
instance Show (ApiException m r) where
show (ApiException _) = "ApiException"
instance (Typeable m, Typeable r) => Exception (ApiException m r) where
handleApiException :: (query ~ '[], Monad (HandlerM p)) => p -> ApiException m r -> (HandlerM p) (Query (Response m r) query)
handleApiException _ = return . Failure . Left . apiException
handleSomeException :: (query ~ '[], Monad (HandlerM p)) => p -> SomeException -> (HandlerM p) (Query (Response m r) query)
handleSomeException _ = return . Failure . Right . OtherError
getCookie :: Wai.Request -> Maybe ByteString
getCookie = fmap snd . find ((== hCookie) . fst) . Wai.requestHeaders
getAccept :: Wai.Request -> Maybe ByteString
getAccept = fmap snd . find ((== hAccept) . fst) . Wai.requestHeaders
hSetCookie :: HeaderName
hSetCookie = "Set-Cookie"
getContentType :: ResponseHeaders -> Maybe ByteString
getContentType = fmap snd . find ((== hContentType) . fst)
newtype Tagged (s :: [*]) b = Tagged { unTagged :: b }
toTagged :: Proxy s -> b -> Tagged s b
toTagged _ = Tagged