diff --git a/servant-client-core/src/Servant/Client/Core/HasClient.hs b/servant-client-core/src/Servant/Client/Core/HasClient.hs index f3a53ad58..5b00b1fc0 100644 --- a/servant-client-core/src/Servant/Client/Core/HasClient.hs +++ b/servant-client-core/src/Servant/Client/Core/HasClient.hs @@ -1,5 +1,6 @@ {-# LANGUAGE CPP #-} {-# OPTIONS_GHC -Wno-missing-methods #-} +{-# LANGUAGE EmptyCase #-} module Servant.Client.Core.HasClient ( clientIn, HasClient (..), @@ -70,7 +71,7 @@ import Servant.API.Generic (GenericMode(..), ToServant, ToServantApi , GenericServant, toServant, fromServant) import Servant.API.ContentTypes - (contentTypes, AllMime (allMime), AllMimeUnrender (allMimeUnrender)) + (contentTypes, AllMime (allMime), AllMimeUnrender (allMimeUnrender), AcceptHeader) import Servant.API.QueryString (ToDeepQuery(..), generateDeepParam) import Servant.API.Status (statusFromNat) @@ -87,6 +88,8 @@ import Servant.Client.Core.ClientError import Servant.Client.Core.Request import Servant.Client.Core.Response import Servant.Client.Core.RunClient +import Servant.API.MultiVerb +import qualified Network.HTTP.Media as M -- * Accessing APIs as a Client @@ -972,6 +975,64 @@ x // f = f x (/:) :: (a -> b -> c) -> b -> a -> c (/:) = flip +class IsResponseList cs as where + responseListRender :: AcceptHeader -> Union (ResponseTypes as) -> Maybe InternalResponse + responseListUnrender :: M.MediaType -> InternalResponse -> UnrenderResult (Union (ResponseTypes as)) + + responseListStatuses :: [Status] + +instance IsResponseList cs '[] where + responseListRender _ x = case x of {} + responseListUnrender _ _ = empty + responseListStatuses = [] + +instance + ( IsResponseList cs as, + AllMime cs, + ReflectMethod method, + AsUnion as r, + RunClient m + ) => + HasClient m (MultiVerb method cs as r) + where + type Client m (MultiVerb method cs as r) = m r + + clientWithRoute _ _ req = do + response <- + runRequestAcceptStatus + (Just (responseListStatuses @cs @as)) + req + { requestMethod = method, + requestAccept = Seq.fromList accept + } + + c <- getResponseContentType response + unless (any (M.matches c) accept) $ do + throwClientError $ UnsupportedContentType c response + + -- FUTUREWORK: support streaming + let sresp = + if LBS.null (responseBody response) + then SomeResponse response {responseBody = ()} + else SomeResponse response + case responseListUnrender @cs @as c sresp of + StatusMismatch -> throwClientError (DecodeFailure "Status mismatch" response) + UnrenderError e -> throwClientError (DecodeFailure (Text.pack e) response) + UnrenderSuccess x -> pure (fromUnion @as x) + where + accept = allMime (Proxy @cs) + method = reflectMethod (Proxy @method) + + hoistClientMonad _ _ f = f + +getResponseContentType :: (RunClient m) => Response -> m M.MediaType +getResponseContentType response = + case lookup "Content-Type" (toList (responseHeaders response)) of + Nothing -> pure $ "application" M.// "octet-stream" + Just t -> case M.parseAccept t of + Nothing -> throwClientError $ InvalidContentTypeHeader response + Just t' -> pure t' + {- Note [Non-Empty Content Types] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/servant-server/src/Servant/Server/Internal.hs b/servant-server/src/Servant/Server/Internal.hs index 965c626f2..c56730ccd 100644 --- a/servant-server/src/Servant/Server/Internal.hs +++ b/servant-server/src/Servant/Server/Internal.hs @@ -1,4 +1,5 @@ {-# LANGUAGE CPP #-} +{-# LANGUAGE DeriveTraversable #-} module Servant.Server.Internal ( module Servant.Server.Internal @@ -39,10 +40,10 @@ import Data.Tagged import qualified Data.Text as T import Data.Typeable import GHC.Generics -import GHC.TypeLits (KnownNat, KnownSymbol, TypeError, ErrorMessage (..), symbolVal) +import GHC.TypeLits (KnownNat, KnownSymbol, TypeError, ErrorMessage (..), symbolVal, Nat) import qualified Network.HTTP.Media as NHM import Network.HTTP.Types hiding - (Header, ResponseHeaders) + (statusCode, Header, ResponseHeaders) import Network.Socket (SockAddr) import Network.Wai @@ -62,7 +63,7 @@ import Servant.API.Generic (GenericMode(..), ToServant, ToServantApi, import Servant.API.ContentTypes (AcceptHeader (..), AllCTRender (..), AllCTUnrender (..), AllMime, MimeRender (..), MimeUnrender (..), NoContent, - canHandleAcceptH) + canHandleAcceptH, AllMimeRender, AllMimeUnrender) import Servant.API.Modifiers (FoldLenient, FoldRequired, RequestArgument, unfoldRequestArgument) @@ -70,12 +71,18 @@ import Servant.API.QueryString (FromDeepQuery(..)) import Servant.API.ResponseHeaders (GetHeaders, Headers, getHeaders, getResponse) import Servant.API.Status - (statusFromNat) + (statusFromNat, KnownStatus) import qualified Servant.Types.SourceT as S import Servant.API.TypeErrors import Web.HttpApiData (FromHttpApiData, parseHeader, parseQueryParam, parseUrlPiece, parseUrlPieces) +import Network.HTTP.Types (Header) +import Data.Sequence (Seq) +import qualified Network.Wai as Wai +import Data.ByteString (ByteString) +import qualified Network.HTTP.Media as M +import Control.Applicative (Alternative) import Servant.Server.Internal.BasicAuth import Servant.Server.Internal.Context @@ -87,8 +94,9 @@ import Servant.Server.Internal.Router import Servant.Server.Internal.RouteResult import Servant.Server.Internal.RoutingApplication import Servant.Server.Internal.ServerError - +import Servant.API.MultiVerb import Servant.API.TypeLevel (AtMostOneFragment, FragmentUnique) +import Data.SOP class HasServer api context where -- | The type of a server for this API, given a monad to run effects in. @@ -1114,3 +1122,236 @@ instance toServant server servantSrvN :: ServerT (ToServantApi api) n = hoistServerWithContext (Proxy @(ToServantApi api)) pctx nat servantSrvM + +data InternalResponse a = InternalResponse + { statusCode :: Status + , headers :: Seq Header + , responseBody :: a + } deriving stock (Eq, Show, Generic, Typeable, Functor, Foldable, Traversable) + +class (Typeable a) => IsWaiBody a where + responseToWai :: InternalResponse a -> Wai.Response + +instance IsWaiBody BSL.ByteString where + responseToWai r = + Wai.responseLBS + (statusCode r) + (toList (headers r)) + (responseBody r) + +instance IsWaiBody () where + responseToWai r = + Wai.responseLBS + (statusCode r) + (toList (headers r)) + mempty + +instance IsWaiBody (SourceIO ByteString) where + responseToWai r = + Wai.responseStream + (statusCode r) + (toList (headers r)) + $ \output flush -> do + foreach + (const (pure ())) + (\chunk -> output (byteString chunk) *> flush) + (responseBody r) + + +class (IsWaiBody (ResponseBody a)) => IsResponse cs a where + type ResponseStatus a :: Nat + type ResponseBody a :: Type + + responseRender :: AcceptHeader -> ResponseType a -> Maybe (InternalResponse (ResponseBody a)) + responseUnrender :: M.MediaType -> InternalResponse (ResponseBody a) -> UnrenderResult (ResponseType a) + +data SomeResponse = forall a. (IsWaiBody a) => SomeResponse (InternalResponse a) + +addContentType :: forall ct a. (Accept ct) => InternalResponse a -> InternalResponse a +addContentType = addContentType' (contentType (Proxy @ct)) + +addContentType' :: M.MediaType -> InternalResponse a -> InternalResponse a +addContentType' c r = r {headers = (hContentType, M.renderHeader c) <| headers r} + +setEmptyBody :: SomeResponse -> SomeResponse +setEmptyBody (SomeResponse r) = SomeResponse (go r) + where + go :: InternalResponse a -> InternalResponse BSL.ByteString + go InternalResponse {..} = InternalResponse {responseBody = mempty, ..} + +someResponseToWai :: SomeResponse -> Wai.Response +someResponseToWai (SomeResponse r) = responseToWai r + +fromSomeResponse :: (Alternative m, Typeable a) => SomeResponse -> m (InternalResponse a) +fromSomeResponse (SomeResponse InternalResponse {..}) = do + body <- maybe empty pure $ cast responseBody + pure $ + InternalResponse + { responseBody = body, + .. + } + +instance + ( KnownStatus s, + MimeRender ct a, + MimeUnrender ct a + ) => + IsResponse cs (RespondAs (ct :: Type) s desc a) + where + type ResponseStatus (RespondAs ct s desc a) = s + type ResponseBody (RespondAs ct s desc a) = BSL.ByteString + + responseRender _ x = + pure . addContentType @ct $ + InternalResponse + { statusCode = statusVal (Proxy @s), + responseBody = mimeRender (Proxy @ct) x, + headers = mempty + } + + responseUnrender _ output = do + guard (statusCode output == statusVal (Proxy @s)) + either UnrenderError UnrenderSuccess $ + mimeUnrender (Proxy @ct) (responseBody output) + +instance (KnownStatus s) => IsResponse cs (RespondAs '() s desc ()) where + type ResponseStatus (RespondAs '() s desc ()) = s + type ResponseBody (RespondAs '() s desc ()) = () + + responseRender _ _ = + pure $ + InternalResponse + { statusCode = statusVal (Proxy @s), + responseBody = (), + headers = mempty + } + + responseUnrender _ output = + guard (statusCode output == statusVal (Proxy @s)) + +instance + (Accept ct, KnownStatus s) => + IsResponse cs (RespondStreaming s desc framing ct) + where + type ResponseStatus (RespondStreaming s desc framing ct) = s + type ResponseBody (RespondStreaming s desc framing ct) = SourceIO ByteString + responseRender _ x = + pure . addContentType @ct $ + InternalResponse + { statusCode = statusVal (Proxy @s), + responseBody = x + } + + responseUnrender _ resp = do + guard (statusCode resp == statusVal (Proxy @s)) + pure $ responseBody resp + +instance (AllMimeRender cs a, AllMimeUnrender cs a, KnownStatus s) => IsResponse cs (Respond s desc a) where + type ResponseStatus (Respond s desc a) = s + type ResponseBody (Respond s desc a) = BSL.ByteString + + -- Note: here it seems like we are rendering for all possible content types, + -- only to choose the correct one afterwards. However, render results besides the + -- one picked by 'M.mapAcceptMedia' are not evaluated, and therefore nor are the + -- corresponding rendering functions. + responseRender (AcceptHeader acc) x = + M.mapAcceptMedia (map (uncurry mkRenderOutput) (allMimeRender (Proxy @cs) x)) acc + where + mkRenderOutput :: M.MediaType -> BSL.ByteString -> (M.MediaType, Response) + mkRenderOutput c body = + (c,) . addContentType' c $ + InternalResponse + { statusCode = statusVal (Proxy @s), + responseBody = body, + headers = mempty + } + + responseUnrender c output = do + guard (statusCode output == statusVal (Proxy @s)) + let results = allMimeUnrender (Proxy @cs) + case lookup c results of + Nothing -> empty + Just f -> either UnrenderError UnrenderSuccess (f (responseBody output)) + +instance + ( HasAcceptCheck cs, + IsResponseList cs as, + AsUnion as r, + ReflectMethod method + ) => + HasServer (MultiVerb method cs as r) ctx + where + type ServerT (MultiVerb method cs as r) m = m r + + hoistServerWithContext _ _ f = f + + route :: + forall env. + Proxy (MultiVerb method cs as r) -> + Context ctx -> + Delayed env (Handler r) -> + Router env + route _ _ action = leafRouter $ \env req k -> do + let acc = getAcceptHeader req + action' = + action + `addMethodCheck` methodCheck method req + `addAcceptCheck` acceptCheck' (Proxy @cs) acc + runAction action' env req k $ \output -> do + let mresp = responseListRender @cs @as acc (toUnion @as output) + someResponseToWai <$> case mresp of + Nothing -> FailFatal err406 + Just resp + | allowedMethodHead method req -> pure (setEmptyBody resp) + | otherwise -> pure resp + where + method = reflectMethod (Proxy @method) + +instance + ( AsHeaders xs (ResponseType r) a, + ServantHeaders hs xs, + IsResponse cs r + ) => + IsResponse cs (WithHeaders hs a r) + where + type ResponseStatus (WithHeaders hs a r) = ResponseStatus r + type ResponseBody (WithHeaders hs a r) = ResponseBody r + + responseRender acc x = addHeaders <$> responseRender @cs @r acc y + where + (hs, y) = toHeaders @xs x + addHeaders r = + r + { headers = headers r <> Seq.fromList (constructHeaders @hs hs) + } + + responseUnrender c output = do + x <- responseUnrender @cs @r c output + case extractHeaders @hs (headers output) of + Nothing -> UnrenderError "Failed to parse headers" + Just hs -> pure $ fromHeaders @xs (hs, x) + +instance + ( IsResponse cs a, + KnownStatus (ResponseStatus a) + ) => + IsResponseList cs (a ': as) + where + responseListRender acc (Z (I x)) = fmap SomeResponse (responseRender @cs @a acc x) + responseListRender acc (S x) = responseListRender @cs @as acc x + + responseListUnrender c output = + Z . I <$> (responseUnrender @cs @a c =<< fromSomeResponse output) + <|> S <$> responseListUnrender @cs @as c output + + responseListStatuses = statusVal (Proxy @(ResponseStatus a)) : responseListStatuses @cs @as + +class HasAcceptCheck cs where + acceptCheck' :: Proxy cs -> AcceptHeader -> DelayedIO () + +instance (AllMime cs) => HasAcceptCheck cs where + acceptCheck' = acceptCheck + +instance HasAcceptCheck '() where + acceptCheck' _ _ = pure () + diff --git a/servant/servant.cabal b/servant/servant.cabal index 4ae3c0dbc..1bd5e7303 100644 --- a/servant/servant.cabal +++ b/servant/servant.cabal @@ -108,10 +108,10 @@ library Servant.API.TypeLevel Servant.API.TypeLevel.List Servant.API.UVerb + Servant.API.MultiVerb Servant.API.UVerb.Union Servant.API.Vault Servant.API.Verbs - Servant.API.MultiVerb Servant.API.WithNamedContext Servant.API.WithResource diff --git a/servant/src/Servant/API/MultiVerb.hs b/servant/src/Servant/API/MultiVerb.hs index 6efef6e9d..aee2d855c 100644 --- a/servant/src/Servant/API/MultiVerb.hs +++ b/servant/src/Servant/API/MultiVerb.hs @@ -1,21 +1,6 @@ {-# LANGUAGE ApplicativeDo #-} {-# LANGUAGE EmptyCase #-} --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - module Servant.API.MultiVerb ( -- * MultiVerb types MultiVerb, @@ -38,27 +23,29 @@ module Servant.API.MultiVerb GenericAsUnion (..), ResponseType, ResponseTypes, + UnrenderResult(..) ) where + import Control.Applicative (Alternative(..), empty) -import qualified Data.CaseInsensitive as CI +import Control.Monad (ap, MonadPlus(..)) +import Data.ByteString (ByteString) import Data.Kind import Data.Proxy import Data.SOP import Data.Sequence (Seq(..)) +import GHC.TypeLits +import Generics.SOP as GSOP +import Network.HTTP.Types as HTTP +import Web.HttpApiData (FromHttpApiData, ToHttpApiData, parseHeader, toHeader) +import qualified Data.CaseInsensitive as CI import qualified Data.Sequence as Seq import qualified Data.Text as Text import qualified Data.Text.Encoding as Text -import Generics.SOP as GSOP -import GHC.TypeLits -import Network.HTTP.Types as HTTP -import Data.ByteString (ByteString) -import Control.Monad (ap, MonadPlus(..)) import Servant.API.TypeLevel.List import Servant.API.Stream (SourceIO) -import Web.HttpApiData (FromHttpApiData, ToHttpApiData, parseHeader, toHeader) import Servant.API.UVerb.Union (Union) import Servant.API.Header (Header') @@ -125,10 +112,12 @@ type family ResponseType a :: Type type instance ResponseType (Respond s desc a) = a + type instance ResponseType (RespondAs ct s desc a) = a type instance ResponseType (RespondStreaming s desc framing ct) = SourceIO ByteString + -- | This type adds response headers to a 'MultiVerb' response. -- -- Type variables: @@ -216,10 +205,12 @@ instance (ServantHeader h name x) => ServantHeader (OptHeader h) name (Maybe x) type instance ResponseType (WithHeaders hs a r) = a + type family ResponseTypes (as :: [Type]) where ResponseTypes '[] = '[] ResponseTypes (a ': as) = ResponseType a ': ResponseTypes as + -- | This type can be used in Servant to produce an endpoint which can return -- multiple values with various content types and status codes. It is similar to -- 'UVerb' and behaves similarly, but it has some important differences: @@ -237,7 +228,6 @@ data MultiVerb (method :: StdMethod) cs (as :: [Type]) (r :: Type) -- | A 'MultiVerb' endpoint with a single response. type MultiVerb1 m cs a = MultiVerb m cs '[a] (ResponseType a) - -- | This class is used to convert a handler return type to a union type -- including all possible responses of a 'MultiVerb' endpoint. -- @@ -316,9 +306,13 @@ maybeFromUnion :: (EitherFromUnion as '[()]) => (Union as -> a) -> (Union (as .++ '[()]) -> Maybe a) -maybeFromUnion f = leftToMaybe . eitherFromUnion @as @'[()] f (const (Z (I ()))) - where - leftToMaybe = either Just (const Nothing) +maybeFromUnion f = + leftToMaybe . eitherFromUnion @as @'[()] f (const (Z (I ()))) + where + leftToMaybe = either Just (const Nothing) + + + -- | This class can be instantiated to get automatic derivation of 'AsUnion' -- instances via 'GenericAsUnion'. The idea is that one has to make sure that for @@ -420,6 +414,7 @@ instance -- "failure" case, normally represented by 'Nothing', corresponds to the /first/ -- response. instance + {-# OVERLAPPABLE #-} (ResponseType r1 ~ (), ResponseType r2 ~ a) => AsUnion '[r1, r2] (Maybe a) where diff --git a/servant/src/Servant/API/TypeLevel.hs b/servant/src/Servant/API/TypeLevel.hs index 42ee42bd1..9e2f91119 100644 --- a/servant/src/Servant/API/TypeLevel.hs +++ b/servant/src/Servant/API/TypeLevel.hs @@ -47,7 +47,7 @@ module Servant.API.TypeLevel ( And, -- ** Fragment FragmentUnique, - AtMostOneFragment, + AtMostOneFragment ) where @@ -72,11 +72,12 @@ import Servant.API.Generic (ToServantApi) import Servant.API.Sub (type (:>)) +import Servant.API.Verbs + (Verb) import Servant.API.UVerb (UVerb) import GHC.TypeLits (ErrorMessage (..), TypeError) -import Servant.API.MultiVerb @@ -145,7 +146,7 @@ type family IsElem endpoint api :: Constraint where IsElem sa (QueryParams x y :> sb) = IsElem sa sb IsElem sa (QueryFlag x :> sb) = IsElem sa sb IsElem sa (Fragment x :> sb) = IsElem sa sb - IsElem (MultiVerb m s ct typ) (MultiVerb m s ct' typ) + IsElem (Verb m s ct typ) (Verb m s ct' typ) = IsSubList ct ct' IsElem e e = () IsElem e (NamedRoutes rs) = IsElem e (ToServantApi rs) @@ -272,7 +273,7 @@ families are not evaluated (see https://ghc.haskell.org/trac/ghc/ticket/12048). -- ... class FragmentUnique api => AtMostOneFragment api -instance AtMostOneFragment (MultiVerb m s ct typ) +instance AtMostOneFragment (Verb m s ct typ) instance AtMostOneFragment (UVerb m cts as) diff --git a/servant/src/Servant/API/Verbs.hs b/servant/src/Servant/API/Verbs.hs index 29b3aba9d..fbe77e656 100644 --- a/servant/src/Servant/API/Verbs.hs +++ b/servant/src/Servant/API/Verbs.hs @@ -22,16 +22,14 @@ import Network.HTTP.Types.Method (Method, StdMethod (..), methodConnect, methodDelete, methodGet, methodHead, methodOptions, methodPatch, methodPost, methodPut, methodTrace) -import Servant.API.MultiVerb (MultiVerb1, Respond) -- | @Verb@ is a general type for representing HTTP verbs (a.k.a. methods). For -- convenience, type synonyms for each verb with a 200 response code are -- provided, but you are free to define your own: -- -- >>> type Post204 contentTypes a = Verb 'POST 204 contentTypes a --- data Verb (method :: k1) (statusCode :: Nat) (contentTypes :: [Type]) (a :: Type) -type Verb (method :: StdMethod) (statusCode :: Nat) (contentTypes :: [Type]) (returnType :: Type) - = MultiVerb1 method contentTypes (Respond statusCode "" returnType) +data Verb (method :: k1) (statusCode :: Nat) (contentTypes :: [Type]) (a :: Type) + deriving (Typeable, Generic) -- | @NoContentVerb@ is a specific type to represent 'NoContent' responses. -- It does not require either a list of content types (because there's @@ -49,15 +47,15 @@ data NoContentVerb (method :: k1) -- the relevant information is summarily presented here. -- | 'GET' with 200 status code. -type Get contentTypes returnType = Verb 'GET 200 contentTypes returnType +type Get = Verb 'GET 200 -- | 'POST' with 200 status code. -type Post contentTypes returnType = Verb 'POST 200 contentTypes returnType +type Post = Verb 'POST 200 -- | 'PUT' with 200 status code. -type Put contentTypes returnType = Verb 'PUT 200 contentTypes returnType +type Put = Verb 'PUT 200 -- | 'DELETE' with 200 status code. -type Delete contentTypes returnType = Verb 'DELETE 200 contentTypes returnType +type Delete = Verb 'DELETE 200 -- | 'PATCH' with 200 status code. -type Patch contentTypes returnType = Verb 'PATCH 200 contentTypes returnType +type Patch = Verb 'PATCH 200 -- * Other responses @@ -74,9 +72,9 @@ type Patch contentTypes returnType = Verb 'PATCH 200 contentTypes returnType -- field. -- | 'POST' with 201 status code. -type PostCreated contentTypes returnType = Verb 'POST 201 contentTypes returnType +type PostCreated = Verb 'POST 201 -- | 'PUT' with 201 status code. -type PutCreated contentTypes returnType = Verb 'PUT 201 contentTypes returnType +type PutCreated = Verb 'PUT 201 -- ** 202 Accepted @@ -87,15 +85,15 @@ type PutCreated contentTypes returnType = Verb 'PUT 201 contentTypes returnType -- estimate of when the processing will be finished. -- | 'GET' with 202 status code. -type GetAccepted contentTypes returnType = Verb 'GET 202 contentTypes returnType +type GetAccepted = Verb 'GET 202 -- | 'POST' with 202 status code. -type PostAccepted contentTypes returnType = Verb 'POST 202 contentTypes returnType +type PostAccepted = Verb 'POST 202 -- | 'DELETE' with 202 status code. -type DeleteAccepted contentTypes returnType = Verb 'DELETE 202 contentTypes returnType +type DeleteAccepted = Verb 'DELETE 202 -- | 'PATCH' with 202 status code. -type PatchAccepted contentTypes returnType = Verb 'PATCH 202 contentTypes returnType +type PatchAccepted = Verb 'PATCH 202 -- | 'PUT' with 202 status code. -type PutAccepted contentTypes returnType = Verb 'PUT 202 contentTypes returnType +type PutAccepted = Verb 'PUT 202 -- ** 203 Non-Authoritative Information @@ -104,15 +102,15 @@ type PutAccepted contentTypes returnType = Verb 'PUT 202 contentTypes returnT -- information may come from a third-party. -- | 'GET' with 203 status code. -type GetNonAuthoritative contentTypes returnType = Verb 'GET 203 contentTypes returnType +type GetNonAuthoritative = Verb 'GET 203 -- | 'POST' with 203 status code. -type PostNonAuthoritative contentTypes returnType = Verb 'POST 203 contentTypes returnType +type PostNonAuthoritative = Verb 'POST 203 -- | 'DELETE' with 203 status code. -type DeleteNonAuthoritative contentTypes returnType = Verb 'DELETE 203 contentTypes returnType +type DeleteNonAuthoritative = Verb 'DELETE 203 -- | 'PATCH' with 203 status code. -type PatchNonAuthoritative contentTypes returnType = Verb 'PATCH 203 contentTypes returnType +type PatchNonAuthoritative = Verb 'PATCH 203 -- | 'PUT' with 203 status code. -type PutNonAuthoritative contentTypes returnType = Verb 'PUT 203 contentTypes returnType +type PutNonAuthoritative = Verb 'PUT 203 -- ** 204 No Content @@ -143,15 +141,15 @@ type HeadNoContent = NoContentVerb 'HEAD -- If the document view should not be reset, use @204 No Content@. -- | 'GET' with 205 status code. -type GetResetContent contentTypes returnType = Verb 'GET 205 contentTypes returnType +type GetResetContent = Verb 'GET 205 -- | 'POST' with 205 status code. -type PostResetContent contentTypes returnType = Verb 'POST 205 contentTypes returnType +type PostResetContent = Verb 'POST 205 -- | 'DELETE' with 205 status code. -type DeleteResetContent contentTypes returnType = Verb 'DELETE 205 contentTypes returnType +type DeleteResetContent = Verb 'DELETE 205 -- | 'PATCH' with 205 status code. -type PatchResetContent contentTypes returnType = Verb 'PATCH 205 contentTypes returnType +type PatchResetContent = Verb 'PATCH 205 -- | 'PUT' with 205 status code. -type PutResetContent contentTypes returnType = Verb 'PUT 205 contentTypes returnType +type PutResetContent = Verb 'PUT 205 -- ** 206 Partial Content @@ -163,7 +161,7 @@ type PutResetContent contentTypes returnType = Verb 'PUT 205 contentTypes ret -- RFC7233 Section 4.1> -- | 'GET' with 206 status code. -type GetPartialContent contentTypes returnType = Verb 'GET 206 contentTypes returnType +type GetPartialContent = Verb 'GET 206 class ReflectMethod a where diff --git a/servant/src/Servant/Links.hs b/servant/src/Servant/Links.hs index 7a2536594..1dc594763 100644 --- a/servant/src/Servant/Links.hs +++ b/servant/src/Servant/Links.hs @@ -177,13 +177,13 @@ import Servant.API.UVerb import Servant.API.Vault (Vault) import Servant.API.Verbs - (NoContentVerb) + (Verb, NoContentVerb) import Servant.API.WithNamedContext (WithNamedContext) import Servant.API.WithResource (WithResource) import Web.HttpApiData -import Servant.API.MultiVerb (MultiVerb) +import Servant.API.MultiVerb -- | A safe link datatype. -- The only way of constructing a 'Link' is using 'safeLink', which means any @@ -565,8 +565,8 @@ instance HasLink EmptyAPI where toLink _ _ _ = EmptyAPI -- Verb (terminal) instances -instance HasLink (MultiVerb m s ct a) where - type MkLink (MultiVerb m s ct a) r = r +instance HasLink (Verb m s ct a) where + type MkLink (Verb m s ct a) r = r toLink toA _ = toA instance HasLink (NoContentVerb m) where @@ -647,6 +647,7 @@ simpleToLink _ toA _ = toLink toA (Proxy :: Proxy sub) -- $setup -- >>> import Servant.API -- >>> import Data.Text (Text) + -- Erroring instance for 'HasLink' when a combinator is not fully applied instance TypeError (PartialApplication #if __GLASGOW_HASKELL__ >= 904 @@ -665,3 +666,7 @@ instance {-# OVERLAPPABLE #-} TypeError (NoInstanceForSub HasLink ty) => HasLink (ty :> sub) instance {-# OVERLAPPABLE #-} TypeError (NoInstanceFor (HasLink api)) => HasLink api + +instance HasLink (MultiVerb method cs as r) where + type MkLink (MultiVerb method cs as r) a = a + toLink toA _ = toA