diff --git a/servant-checked-exceptions-core/servant-checked-exceptions-core.cabal b/servant-checked-exceptions-core/servant-checked-exceptions-core.cabal index 9b4d6cc..c97cf23 100644 --- a/servant-checked-exceptions-core/servant-checked-exceptions-core.cabal +++ b/servant-checked-exceptions-core/servant-checked-exceptions-core.cabal @@ -26,6 +26,7 @@ library , Servant.Checked.Exceptions.Internal , Servant.Checked.Exceptions.Internal.Envelope , Servant.Checked.Exceptions.Internal.EnvelopeT + , Servant.Checked.Exceptions.Internal.FlatEnvelope , Servant.Checked.Exceptions.Internal.Prism , Servant.Checked.Exceptions.Internal.Servant , Servant.Checked.Exceptions.Internal.Servant.API diff --git a/servant-checked-exceptions-core/src/Servant/Checked/Exceptions.hs b/servant-checked-exceptions-core/src/Servant/Checked/Exceptions.hs index 1b94b3c..93eee00 100644 --- a/servant-checked-exceptions-core/src/Servant/Checked/Exceptions.hs +++ b/servant-checked-exceptions-core/src/Servant/Checked/Exceptions.hs @@ -101,13 +101,16 @@ module Servant.Checked.Exceptions -- * Servant Types -- ** 'Throws' API parameter Throws + , Throws' -- ** 'NoThrow' API parameter , NoThrow + , NoThrow' -- ** HTTP Error Status Code , ErrStatus(toErrStatus) , Status -- ** Verbs , VerbWithErr + , VerbWithErr' -- *** Specialized Verbs -- **** HTTP 200 , GetWithErr @@ -145,6 +148,7 @@ module Servant.Checked.Exceptions , GetPartialContentWithErr -- * 'Envelope' response wrapper , Envelope(..) + , FlatEnvelope(..) -- ** 'Envelope' helper functions -- *** 'Envelope' constructors , toSuccEnvelope diff --git a/servant-checked-exceptions-core/src/Servant/Checked/Exceptions/Internal.hs b/servant-checked-exceptions-core/src/Servant/Checked/Exceptions/Internal.hs index 49dc99b..912f496 100644 --- a/servant-checked-exceptions-core/src/Servant/Checked/Exceptions/Internal.hs +++ b/servant-checked-exceptions-core/src/Servant/Checked/Exceptions/Internal.hs @@ -1,6 +1,7 @@ module Servant.Checked.Exceptions.Internal ( module Servant.Checked.Exceptions.Internal.Envelope , module Servant.Checked.Exceptions.Internal.EnvelopeT + , module Servant.Checked.Exceptions.Internal.FlatEnvelope , module Servant.Checked.Exceptions.Internal.Verbs , module Servant.Checked.Exceptions.Internal.Servant , module Servant.Checked.Exceptions.Internal.Util @@ -8,6 +9,7 @@ module Servant.Checked.Exceptions.Internal import Servant.Checked.Exceptions.Internal.Envelope import Servant.Checked.Exceptions.Internal.EnvelopeT +import Servant.Checked.Exceptions.Internal.FlatEnvelope import Servant.Checked.Exceptions.Internal.Verbs import Servant.Checked.Exceptions.Internal.Servant import Servant.Checked.Exceptions.Internal.Util diff --git a/servant-checked-exceptions-core/src/Servant/Checked/Exceptions/Internal/FlatEnvelope.hs b/servant-checked-exceptions-core/src/Servant/Checked/Exceptions/Internal/FlatEnvelope.hs new file mode 100644 index 0000000..dbe623a --- /dev/null +++ b/servant-checked-exceptions-core/src/Servant/Checked/Exceptions/Internal/FlatEnvelope.hs @@ -0,0 +1,59 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE InstanceSigs #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE PolyKinds #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE UndecidableInstances #-} + +{- | +Module : Servant.Checked.Exceptions.Internal.Servant.API + +This module defines the 'FlatEnvelope' type. +-} + +module Servant.Checked.Exceptions.Internal.FlatEnvelope + ( FlatEnvelope(..)) + where + +import Control.Applicative ((<|>)) +import Data.Aeson (ToJSON(..), FromJSON(..)) +import Data.WorldPeace (OpenUnion) + +import Servant.Checked.Exceptions.Internal.Servant.API (EnvelopeStatus(..), AllErrStatus, MkEnvelope(..)) +import Servant.Checked.Exceptions.Internal.Envelope (Envelope(..), envelope, toErrEnvelope, toSuccEnvelope) + +-- | Wrapper around @Envelope@ that has a flat JSON representation. +-- While with @Envelope@ the data and errors are contained in "err" and "data" +-- fields, with @FlatEnvelope@ they are both contained in the root dictionary. +data FlatEnvelope (es :: [*]) (succ :: *) = + FlatEnvelope + { unFlatEnvelope :: Envelope es succ + } + +instance (Show (OpenUnion es), Show a) => Show (FlatEnvelope es a) where + show = envelope show show . unFlatEnvelope + +-- | Both the error and success values are in the top level of the json. +instance (ToJSON (OpenUnion es), ToJSON a) => ToJSON (FlatEnvelope es a) where + toJSON = envelope toJSON toJSON . unFlatEnvelope + +-- | Both the error and success values are in the top level of the json. +-- Success values are tried first, then errors. +instance (FromJSON (OpenUnion es), FromJSON a) => FromJSON (FlatEnvelope es a) where + parseJSON v = FlatEnvelope <$> + ( SuccEnvelope <$> parseJSON v + <|> ErrEnvelope <$> parseJSON v + ) + +instance AllErrStatus es => EnvelopeStatus es FlatEnvelope where + getEnvelopeStatus (FlatEnvelope envel) = getEnvelopeStatus envel + +instance MkEnvelope FlatEnvelope where + mkSuccEnvelope = FlatEnvelope . toSuccEnvelope + mkErrEnvelope = FlatEnvelope . toErrEnvelope diff --git a/servant-checked-exceptions-core/src/Servant/Checked/Exceptions/Internal/Servant/API.hs b/servant-checked-exceptions-core/src/Servant/Checked/Exceptions/Internal/Servant/API.hs index d866dad..ab831a9 100644 --- a/servant-checked-exceptions-core/src/Servant/Checked/Exceptions/Internal/Servant/API.hs +++ b/servant-checked-exceptions-core/src/Servant/Checked/Exceptions/Internal/Servant/API.hs @@ -1,9 +1,12 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE PolyKinds #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} +{-# LANGUAGE UndecidableInstances #-} {- | Module : Servant.Checked.Exceptions.Internal.Servant.API @@ -15,7 +18,8 @@ Maintainer : Dennis Gosnell (cdep.illabout@gmail.com) Stability : experimental Portability : unknown -This module defines the 'Throws' and 'Throwing' types. +This module defines the 'Throws' and 'Throwing' types, and their +envelope-polymorphic variants 'Throws\'' and 'Throwing\'' -} module Servant.Checked.Exceptions.Internal.Servant.API where @@ -23,8 +27,11 @@ module Servant.Checked.Exceptions.Internal.Servant.API where import GHC.Exts (Constraint) import Network.HTTP.Types (Status) import Servant.API ((:>)) +import Data.Functor.Identity (Identity (Identity)) +import Data.WorldPeace (OpenUnion, Union (That, This)) import Servant.Checked.Exceptions.Internal.Util (Snoc) +import Servant.Checked.Exceptions.Internal.Envelope (Envelope, envelope, toSuccEnvelope, toErrEnvelope) -- | 'Throws' is used in Servant API definitions and signifies that an API will -- throw the given error. @@ -34,7 +41,13 @@ import Servant.Checked.Exceptions.Internal.Util (Snoc) -- -- >>> import Servant.API (Get, JSON, (:>)) -- >>> type API = Throws String :> Get '[JSON] Int -data Throws (e :: *) +-- +-- @Throws@ is a specialized case for @Throws'@, that accepts an additional +-- envelope parameter @envel@. For @Throws@ this is @Envelope@. +-- All combinators with a prime are polymorphic in the envelope. +-- For an example of a custom envelope, take a look at 'Servant.Checked.Exceptions.Internal.FlatEnvelope' +type Throws (e :: *) = Throws' Envelope e +data Throws' (env :: [*] -> * -> *) (e :: *) -- | 'NoThrow' is used to indicate that an API will not throw an error, but -- that it will still return a response wrapped in a @@ -53,19 +66,22 @@ data Throws (e :: *) -- apiHandler :: 'Servant.Handler' ('Servant.Checked.Exceptions.Internal.Envelope.Envelope' \'[] Int) -- apiHandler = 'Servant.Checked.Exceptions.Internal.Envelope.pureSuccEnvelope' 3 -- @ -data NoThrow +data NoThrow' (env :: [*] -> * -> *) +type NoThrow = NoThrow' Envelope -- | This is used internally and should not be used by end-users. -data Throwing (e :: [*]) +data Throwing' (env :: [*] -> * -> *) (e :: [*]) +type Throwing (e :: [*]) = Throwing' Envelope e + -- | Used by the 'HasServer' and 'HasClient' instances for -- @'Throwing' es ':>' api ':>' apis@ to detect @'Throwing' es@ followed -- immediately by @'Throws' e@. type family ThrowingNonterminal api where - ThrowingNonterminal (Throwing es :> Throws e :> api) = - Throwing (Snoc es e) :> api - ThrowingNonterminal (Throwing es :> c :> api) = - c :> Throwing es :> api + ThrowingNonterminal (Throwing' env es :> Throws' env e :> api) = + Throwing' env (Snoc es e) :> api + ThrowingNonterminal (Throwing' env es :> c :> api) = + c :> Throwing' env es :> api -- | Note that clients generated by @servant-client@ currently don't handle @@ -78,6 +94,27 @@ type family AllErrStatus (es :: [k]) :: Constraint where AllErrStatus '[] = () AllErrStatus (a ': as) = (ErrStatus a, AllErrStatus as) +-- | Get the HTTP status from an @envelope@. +class EnvelopeStatus es (envel :: [*] -> * -> *) where + getEnvelopeStatus :: envel es a -> Status -> Status + +instance AllErrStatus es => EnvelopeStatus es Envelope where + getEnvelopeStatus envel successStatus = envelope getErrStatus (const successStatus) envel + +-- | Get the HTTP status from an @OpenUnion es@, used by @Envelope@. +getErrStatus :: AllErrStatus es => OpenUnion es -> Status +getErrStatus (This (Identity e)) = toErrStatus e +getErrStatus (That es) = getErrStatus es + +-- | Create an envelope from success or error values. +class MkEnvelope (envel :: [*] -> * -> *) where + mkSuccEnvelope :: a -> envel es a + mkErrEnvelope :: e -> envel '[e] a + +instance MkEnvelope Envelope where + mkSuccEnvelope = toSuccEnvelope + mkErrEnvelope = toErrEnvelope + -- $setup -- >>> :set -XDataKinds -- >>> :set -XTypeOperators diff --git a/servant-checked-exceptions-core/src/Servant/Checked/Exceptions/Internal/Servant/Docs.hs b/servant-checked-exceptions-core/src/Servant/Checked/Exceptions/Internal/Servant/Docs.hs index 264b112..6e20eab 100644 --- a/servant-checked-exceptions-core/src/Servant/Checked/Exceptions/Internal/Servant/Docs.hs +++ b/servant-checked-exceptions-core/src/Servant/Checked/Exceptions/Internal/Servant/Docs.hs @@ -41,123 +41,125 @@ import Servant.Docs ToSample(toSamples)) import Servant.Docs.Internal (apiEndpoints, respBody, response) -import Servant.Checked.Exceptions.Internal.Envelope - (Envelope, toErrEnvelope, toSuccEnvelope) import Servant.Checked.Exceptions.Internal.Prism ((<>~)) import Servant.Checked.Exceptions.Internal.Servant.API - (NoThrow, Throws, Throwing) + (NoThrow', Throws', Throwing', MkEnvelope(..)) import Servant.Checked.Exceptions.Internal.Util (Snoc) -- TODO: Make sure to also account for when headers are being used. -- | Change a 'Throws' into 'Throwing'. -instance (HasDocs (Throwing '[e] :> api)) => HasDocs (Throws e :> api) where +instance (HasDocs (Throwing' envel '[e] :> api)) => HasDocs (Throws' envel e :> api) where docsFor - :: Proxy (Throws e :> api) + :: Proxy (Throws' envel e :> api) -> (Endpoint, Action) -> DocOptions -> API - docsFor Proxy = docsFor (Proxy :: Proxy (Throwing '[e] :> api)) + docsFor Proxy = docsFor (Proxy :: Proxy (Throwing' envel '[e] :> api)) -- | When @'Throwing' es@ comes before a 'Verb', generate the documentation for -- the same 'Verb', but returning an @'Envelope' es@. Also add documentation -- for the potential @es@. instance - ( CreateRespBodiesFor es ctypes - , HasDocs (Verb method status ctypes (Envelope es a)) + ( CreateRespBodiesFor envel es ctypes + , HasDocs (Verb method status ctypes (envel es a)) ) - => HasDocs (Throwing es :> Verb method status ctypes a) where + => HasDocs (Throwing' envel es :> Verb method status ctypes a) where docsFor - :: Proxy (Throwing es :> Verb method status ctypes a) + :: Proxy (Throwing' envel es :> Verb method status ctypes a) -> (Endpoint, Action) -> DocOptions -> API docsFor Proxy (endpoint, action) docOpts = let api = docsFor - (Proxy :: Proxy (Verb method status ctypes (Envelope es a))) + (Proxy :: Proxy (Verb method status ctypes (envel es a))) (endpoint, action) docOpts in api & apiEndpoints . traverse . response . respBody <>~ - createRespBodiesFor (Proxy :: Proxy es) (Proxy :: Proxy ctypes) + createRespBodiesFor (Proxy :: Proxy envel) (Proxy :: Proxy es) (Proxy :: Proxy ctypes) -- | When 'NoThrow' comes before a 'Verb', generate the documentation for -- the same 'Verb', but returning an @'Envelope' \'[]@. -instance (HasDocs (Verb method status ctypes (Envelope '[] a))) - => HasDocs (NoThrow :> Verb method status ctypes a) where +instance (HasDocs (Verb method status ctypes (envel '[] a))) + => HasDocs (NoThrow' envel :> Verb method status ctypes a) where docsFor - :: Proxy (NoThrow :> Verb method status ctypes a) + :: Proxy (NoThrow' envel :> Verb method status ctypes a) -> (Endpoint, Action) -> DocOptions -> API docsFor Proxy (endpoint, action) docOpts = docsFor - (Proxy :: Proxy (Verb method status ctypes (Envelope '[] a))) + (Proxy :: Proxy (Verb method status ctypes (envel '[] a))) (endpoint, action) docOpts --- | Create samples for a given @list@ of types, under given @ctypes@. +-- | When a @'Throws' e@ comes immediately after a @'Throwing' es@, 'Snoc' the +-- @e@ onto the @es@. +instance (HasDocs (Throwing' envel (Snoc es e) :> api)) => + HasDocs (Throwing' envel es :> Throws' envel e :> api) where + docsFor + :: Proxy (Throwing' envel es :> Throws' envel e :> api) + -> (Endpoint, Action) + -> DocOptions + -> API + docsFor Proxy = + docsFor (Proxy :: Proxy (Throwing' envel (Snoc es e) :> api)) + +-- | Create samples for an envelope with a @list@ of types, under given @ctypes@. -- --- Additional instances of this class should not need to be created. -class CreateRespBodiesFor list ctypes where +-- Instances of this class are only necessary when using a custom @envel@. +class CreateRespBodiesFor (envel :: [*] -> * -> *) list ctypes where createRespBodiesFor - :: Proxy list + :: Proxy envel + -> Proxy list -> Proxy ctypes -> [(Text, MediaType, ByteString)] -- | An empty list of types has no samples. -instance CreateRespBodiesFor '[] ctypes where +instance CreateRespBodiesFor (envel :: [*] -> * -> *) '[] ctypes where createRespBodiesFor - :: Proxy '[] + :: Proxy envel + -> Proxy '[] -> Proxy ctypes -> [(Text, MediaType, ByteString)] - createRespBodiesFor Proxy Proxy = [] + createRespBodiesFor Proxy Proxy Proxy = [] -- | Create a response body for each of the error types. instance - ( AllMimeRender ctypes (Envelope '[e] ()) - , CreateRespBodiesFor es ctypes + ( AllMimeRender ctypes (envel '[e] ()) + , CreateRespBodiesFor envel es ctypes , ToSample e + , MkEnvelope envel ) - => CreateRespBodiesFor (e ': es) ctypes where + => CreateRespBodiesFor (envel :: [*] -> * -> *) (e ': es) ctypes where createRespBodiesFor - :: Proxy (e ': es) + :: Proxy envel + -> Proxy (e ': es) -> Proxy ctypes -> [(Text, MediaType, ByteString)] - createRespBodiesFor Proxy ctypes = - createRespBodyFor (Proxy :: Proxy e) ctypes <> - createRespBodiesFor (Proxy :: Proxy es) ctypes + createRespBodiesFor Proxy Proxy ctypes = + createRespBodyFor (Proxy :: Proxy envel) (Proxy :: Proxy e) ctypes <> + createRespBodiesFor (Proxy :: Proxy envel) (Proxy :: Proxy es) ctypes -- | Create a sample for a given @e@ under given @ctypes@. createRespBodyFor - :: forall e ctypes. - (AllMimeRender ctypes (Envelope '[e] ()), ToSample e) - => Proxy e -> Proxy ctypes -> [(Text, MediaType, ByteString)] -createRespBodyFor Proxy ctypes = concatMap enc samples + :: forall (envel :: [*] -> * -> *) e ctypes. + (AllMimeRender ctypes (envel '[e] ()), ToSample e, MkEnvelope envel) + => Proxy envel -> Proxy e -> Proxy ctypes -> [(Text, MediaType, ByteString)] +createRespBodyFor Proxy Proxy ctypes = concatMap enc samples where - samples :: [(Text, Envelope '[e] ())] - samples = fmap toErrEnvelope <$> toSamples (Proxy :: Proxy e) + samples :: [(Text, envel '[e] ())] + samples = fmap mkErrEnvelope <$> toSamples (Proxy :: Proxy e) - enc :: (Text, Envelope '[e] ()) -> [(Text, MediaType, ByteString)] + enc :: (Text, envel '[e] ()) -> [(Text, MediaType, ByteString)] enc (t, s) = uncurry (t,,) <$> allMimeRender ctypes s --- | When a @'Throws' e@ comes immediately after a @'Throwing' es@, 'Snoc' the --- @e@ onto the @es@. -instance (HasDocs (Throwing (Snoc es e) :> api)) => - HasDocs (Throwing es :> Throws e :> api) where - docsFor - :: Proxy (Throwing es :> Throws e :> api) - -> (Endpoint, Action) - -> DocOptions - -> API - docsFor Proxy = - docsFor (Proxy :: Proxy (Throwing (Snoc es e) :> api)) - -- | We can generate a sample of an @'Envelope' es a@ as long as there is a way -- to generate a sample of the @a@. -- -- This doesn't need to worry about generating a sample of @es@, because that is -- taken care of in the 'HasDocs' instance for @'Throwing' es@. -instance ToSample a => ToSample (Envelope es a) where - toSamples :: Proxy (Envelope es a) -> [(Text, Envelope es a)] - toSamples Proxy = fmap toSuccEnvelope <$> toSamples (Proxy :: Proxy a) +instance (ToSample a, MkEnvelope envel) => ToSample ((envel :: [*] -> * -> *) es a) where + toSamples :: Proxy (envel es a) -> [(Text, envel es a)] + toSamples Proxy = fmap mkSuccEnvelope <$> toSamples (Proxy :: Proxy a) diff --git a/servant-checked-exceptions-core/src/Servant/Checked/Exceptions/Internal/Verbs.hs b/servant-checked-exceptions-core/src/Servant/Checked/Exceptions/Internal/Verbs.hs index 29be52e..b067232 100644 --- a/servant-checked-exceptions-core/src/Servant/Checked/Exceptions/Internal/Verbs.hs +++ b/servant-checked-exceptions-core/src/Servant/Checked/Exceptions/Internal/Verbs.hs @@ -25,45 +25,55 @@ import Data.Typeable (Typeable) import GHC.Generics (Generic) import GHC.TypeLits (Nat) import Network.HTTP.Types (StdMethod(DELETE, GET, PATCH, POST, PUT)) +import Servant.Checked.Exceptions.Internal.Envelope (Envelope) -data VerbWithErr +data VerbWithErr' (method :: k1) (successStatusCode :: Nat) (contentTypes :: [*]) + (envel :: [*] -> * -> *) (es :: [*]) a deriving (Generic, Typeable) +type VerbWithErr + (method :: k1) + (successStatusCode :: Nat) + (contentTypes :: [*]) + (es :: [*]) + a + = VerbWithErr' method successStatusCode contentTypes Envelope es a -type GetWithErr = VerbWithErr 'GET 200 -type PostWithErr = VerbWithErr 'POST 200 -type PutWithErr = VerbWithErr 'PUT 200 -type DeleteWithErr = VerbWithErr 'DELETE 200 -type PatchWithErr = VerbWithErr 'PATCH 200 +-- (contentTypes :: [*]) (es :: [*]) a +type GetWithErr (contentTypes :: [*]) (es :: [*]) a = VerbWithErr 'GET 200 contentTypes es a +type PostWithErr (contentTypes :: [*]) (es :: [*]) a = VerbWithErr 'POST 200 contentTypes es a +type PutWithErr (contentTypes :: [*]) (es :: [*]) a = VerbWithErr 'PUT 200 contentTypes es a +type DeleteWithErr (contentTypes :: [*]) (es :: [*]) a = VerbWithErr 'DELETE 200 contentTypes es a +type PatchWithErr (contentTypes :: [*]) (es :: [*]) a = VerbWithErr 'PATCH 200 contentTypes es a -type PostCreatedWithErr = VerbWithErr 'POST 201 +type PostCreatedWithErr (contentTypes :: [*]) (es :: [*]) a = VerbWithErr 'POST 201 contentTypes es a -type GetAcceptedWithErr = VerbWithErr 'GET 202 -type PostAcceptedWithErr = VerbWithErr 'POST 202 -type DeleteAcceptedWithErr = VerbWithErr 'DELETE 202 -type PatchAcceptedWithErr = VerbWithErr 'PATCH 202 -type PutAcceptedWithErr = VerbWithErr 'PUT 202 +type GetAcceptedWithErr (contentTypes :: [*]) (es :: [*]) a = VerbWithErr 'GET 202 contentTypes es a +type PostAcceptedWithErr (contentTypes :: [*]) (es :: [*]) a = VerbWithErr 'POST 202 contentTypes es a +type DeleteAcceptedWithErr (contentTypes :: [*]) (es :: [*]) a = VerbWithErr 'DELETE 202 contentTypes es a +type PatchAcceptedWithErr (contentTypes :: [*]) (es :: [*]) a = VerbWithErr 'PATCH 202 contentTypes es a +type PutAcceptedWithErr (contentTypes :: [*]) (es :: [*]) a = VerbWithErr 'PUT 202 contentTypes es a -type GetNonAuthoritativeWithErr = VerbWithErr 'GET 203 -type PostNonAuthoritativeWithErr = VerbWithErr 'POST 203 -type DeleteNonAuthoritativeWithErr = VerbWithErr 'DELETE 203 -type PatchNonAuthoritativeWithErr = VerbWithErr 'PATCH 203 -type PutNonAuthoritativeWithErr = VerbWithErr 'PUT 203 +type GetNonAuthoritativeWithErr (contentTypes :: [*]) (es :: [*]) a = VerbWithErr 'GET 203 contentTypes es a +type PostNonAuthoritativeWithErr (contentTypes :: [*]) (es :: [*]) a = VerbWithErr 'POST 203 contentTypes es a +type DeleteNonAuthoritativeWithErr (contentTypes :: [*]) (es :: [*]) a = VerbWithErr 'DELETE 203 contentTypes es a +type PatchNonAuthoritativeWithErr (contentTypes :: [*]) (es :: [*]) a = VerbWithErr 'PATCH 203 contentTypes es a +type PutNonAuthoritativeWithErr (contentTypes :: [*]) (es :: [*]) a = VerbWithErr 'PUT 203 contentTypes es a -type GetNoContentWithErr = VerbWithErr 'GET 204 -type PostNoContentWithErr = VerbWithErr 'POST 204 -type DeleteNoContentWithErr = VerbWithErr 'DELETE 204 -type PatchNoContentWithErr = VerbWithErr 'PATCH 204 -type PutNoContentWithErr = VerbWithErr 'PUT 204 +type GetNoContentWithErr (contentTypes :: [*]) (es :: [*]) a = VerbWithErr 'GET 204 contentTypes es a +type PostNoContentWithErr (contentTypes :: [*]) (es :: [*]) a = VerbWithErr 'POST 204 contentTypes es a +type DeleteNoContentWithErr (contentTypes :: [*]) (es :: [*]) a = VerbWithErr 'DELETE 204 contentTypes es a +type PatchNoContentWithErr (contentTypes :: [*]) (es :: [*]) a = VerbWithErr 'PATCH 204 contentTypes es a +type PutNoContentWithErr (contentTypes :: [*]) (es :: [*]) a = VerbWithErr 'PUT 204 contentTypes es a -type GetResetContentWithErr = VerbWithErr 'GET 205 -type PostResetContentWithErr = VerbWithErr 'POST 205 -type DeleteResetContentWithErr = VerbWithErr 'DELETE 205 -type PatchResetContentWithErr = VerbWithErr 'PATCH 205 -type PutResetContentWithErr = VerbWithErr 'PUT 205 +type GetResetContentWithErr (contentTypes :: [*]) (es :: [*]) a = VerbWithErr 'GET 205 contentTypes es a +type PostResetContentWithErr (contentTypes :: [*]) (es :: [*]) a = VerbWithErr 'POST 205 contentTypes es a +type DeleteResetContentWithErr (contentTypes :: [*]) (es :: [*]) a = VerbWithErr 'DELETE 205 contentTypes es a +type PatchResetContentWithErr (contentTypes :: [*]) (es :: [*]) a = VerbWithErr 'PATCH 205 contentTypes es a +type PutResetContentWithErr (contentTypes :: [*]) (es :: [*]) a = VerbWithErr 'PUT 205 contentTypes es a -type GetPartialContentWithErr = VerbWithErr 'GET 206 +type GetPartialContentWithErr (contentTypes :: [*]) (es :: [*]) a = VerbWithErr 'GET 206 contentTypes es a diff --git a/servant-checked-exceptions-core/src/Servant/Checked/Exceptions/Verbs.hs b/servant-checked-exceptions-core/src/Servant/Checked/Exceptions/Verbs.hs index e8b5b1b..b754e4f 100644 --- a/servant-checked-exceptions-core/src/Servant/Checked/Exceptions/Verbs.hs +++ b/servant-checked-exceptions-core/src/Servant/Checked/Exceptions/Verbs.hs @@ -7,6 +7,7 @@ module Servant.Checked.Exceptions.Verbs ( , DeleteWithErr , PatchWithErr , VerbWithErr + , VerbWithErr' -- **** HTTP 201 , PostCreatedWithErr -- **** HTTP 202 diff --git a/servant-checked-exceptions/example/CustomEnvelope.hs b/servant-checked-exceptions/example/CustomEnvelope.hs new file mode 100644 index 0000000..68d0401 --- /dev/null +++ b/servant-checked-exceptions/example/CustomEnvelope.hs @@ -0,0 +1,279 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE TypeSynonymInstances #-} + +module Main where + +import Control.Concurrent (forkIO) +import Control.Monad (guard) +import Data.Aeson (ToJSON(..), (.=), object, FromJSON(..), (.:), withObject, decode) +import Data.Bifunctor (first) +import Data.Functor (($>)) +import Data.Monoid ((<>)) +import Data.Proxy (Proxy(Proxy)) +import qualified Data.Text as T +import Network.HTTP.Client (defaultManagerSettings, newManager) +import Network.HTTP.Types (status400) +import Network.Wai (Application) +import Network.Wai.Handler.Warp (run) +import System.Environment (getArgs) +import Text.Read (readMaybe, readEither) + +import Servant.API (Capture, JSON, Post, (:>), (:<|>)((:<|>)), ToHttpApiData(..), FromHttpApiData(..)) +import Servant.Client + (BaseUrl(BaseUrl), Scheme(Http), ClientEnv(ClientEnv), ClientM, ClientError(FailureResponse), + client, runClientM, responseBody) +import Servant.Docs + (DocCapture(DocCapture), ToCapture(toCapture), ToSample(toSamples), + docs, markdown) +import Servant.Server (Handler, ServerT, serve) + +import Servant.Checked.Exceptions + (Throws', NoThrow', FlatEnvelope(..), ErrStatus(..), + catchesEnvelope, pureSuccEnvelope, emptyEnvelope, pureErrEnvelope) + +------------------------- +-- Types and instances -- +------------------------- + +-- | Dummy Foo parameter type +newtype Foo = Foo Int deriving (Show, Read) + +instance ToHttpApiData Foo where + toUrlPiece (Foo n) = T.pack . show $ n + +instance FromHttpApiData Foo where + parseUrlPiece = fmap Foo . first T.pack . readEither . T.unpack + +-- | Docs for Foo as uri parameter +instance ToCapture (Capture "foo" Foo) where + toCapture Proxy = + DocCapture "foo" "a search string containing an integer" + + +-- | Dummy Bar parameter type +data Bar = Bar deriving (Show, Read) + +instance ToHttpApiData Bar where + toUrlPiece = T.pack . show + +instance FromHttpApiData Bar where + parseUrlPiece = first T.pack . readEither . T.unpack + +-- | Docs for Bar as uri parameter +instance ToCapture (Capture "bar" Bar) where + toCapture Proxy = + DocCapture "bar" "a search string containing \"Bar\"" + + +-- | Dummy FooBar result type +-- When serialised, it is contained inside a "foobar" field. +data FooBar = FooBar String + +instance ToJSON FooBar where + toJSON (FooBar s) = object $ ["foobar" .= s] + +instance FromJSON FooBar where + parseJSON = withObject "FooBar" $ \o -> FooBar <$> o .: "foobar" + +-- | Docs for FooBar as response +instance ToSample FooBar where + toSamples Proxy = [("A successful FooBar response", FooBar "foo: 42")] + +------------ +-- Errors -- +------------ + +-- | Dummy errors: these serialise in an "error" field. +data Err1 = Err1 deriving Show + +instance ToJSON Err1 where + toJSON Err1 = object $ [ "error" .= show Err1] + +instance FromJSON Err1 where + parseJSON = withObject "Err1" $ \o -> do + e <- o .: "error" + guard $ e == show Err1 + pure Err1 + +-- | Error status +instance ErrStatus Err1 where + toErrStatus _ = status400 + +-- | Sample error +instance ToSample Err1 where + toSamples Proxy = [("Error nr. 1", Err1)] + + +data Err2 = Err2 deriving Show + +instance ToJSON Err2 where + toJSON Err2 = object $ ["error" .= show Err2] + +instance FromJSON Err2 where + parseJSON = withObject "Err2" $ \o -> do + e <- o .: "error" + guard $ e == show Err2 + pure Err2 + +instance ErrStatus Err2 where + toErrStatus _ = status400 + +instance ToSample Err2 where + toSamples Proxy = [("Error nr. 2", Err2)] + +--------- +-- API -- +--------- + +-- | This is our main 'Api' type. It defines two simple routes: one throws +-- errors, the second throws no errors. A custom envelope is used for both. +-- We will create a server, a client, and documentation for this api. +-- To understand what is different, compare with the modules 'Api', 'Client' and 'Server'. +type Api = "api" :> ( FooApi :<|> BarApi) + +type FooApi = + Capture "foo" Foo :> + Throws' FlatEnvelope Err1 :> + Throws' FlatEnvelope Err2 :> + Post '[JSON] FooBar + +type BarApi = + Capture "bar" Bar :> + NoThrow' FlatEnvelope :> + Post '[JSON] FooBar + +------------ +-- Server -- +------------ + +-- | This is our server root for the 'ServerT' for 'Api'. We have two handlers. +serverRoot :: ServerT Api Handler +serverRoot = fooHandler :<|> barHandler + +-- | This is the handler for 'FooApi'. +-- If the integer contained in 'Foo' is '1' or '2', we respectively throw an +-- 'Err1' or 'Err2'. Otherwise, we return success. +-- We use standard envelopes, and at the outer layer wrap it with a custom +-- envelope. This allows to reuse existing envelope functions and 'EnvelopeT', +-- but still have custom JSON instances. You can of course define your own +-- custom envelope type which does not use 'Envelope' internally. +fooHandler :: Foo -> Handler (FlatEnvelope '[Err1, Err2] FooBar) +fooHandler (Foo 1) = fmap FlatEnvelope . pureErrEnvelope $ Err1 +fooHandler (Foo 2) = fmap FlatEnvelope . pureErrEnvelope $ Err2 +fooHandler (Foo n) = fmap FlatEnvelope . pureSuccEnvelope . FooBar $ "foo: " <> show n + +-- | This is the handler for 'BarApi'. +-- The handler just returns success, throwing no errors. +barHandler :: Bar -> Handler (FlatEnvelope '[] FooBar) +barHandler Bar = fmap FlatEnvelope . pureSuccEnvelope . FooBar $ "bar" + +-- | Create a WAI 'Application'. +app :: Application +app = serve (Proxy :: Proxy Api) serverRoot + +-- | Run the WAI 'Application' using 'run' on the port defined by 'port'. +serverMain :: IO () +serverMain = run port app + +------------ +-- Client -- +------------ + +-- | We generate the client functions just like normal. Note that when we use +-- 'Throws' or 'NoThrow', the client functions get generated with the +-- 'FlatEnvelope' type. +fooClient + :: Foo + -> ClientM (FlatEnvelope '[Err1, Err2] FooBar) +barClient + :: Bar + -> ClientM (FlatEnvelope '[] FooBar) +(fooClient :<|> barClient) = client (Proxy :: Proxy Api) + +-- | This function uses the 'fooClient' function to send a 'Foo' to +-- the server. +-- +-- Note how 'catchesEnvelope' is used to handle the two error reponses and the +-- success response. +-- To mitigate servant-checked-exceptions#27, we try to reparse the error +-- response on a 'FailureResponse', and get our envelope anyway. +runFoo :: ClientEnv -> Foo -> IO () +runFoo clientEnv foo = do + eitherRes <- runClientM (fooClient foo) clientEnv + case eitherRes of + Left servantErr@(FailureResponse _ resp) -> case decode . responseBody $ resp of + Just fenv -> handleEnvelope fenv + Nothing -> handleServantError servantErr + Left servantErr -> handleServantError servantErr + Right fenv -> handleEnvelope fenv + where + handleEnvelope :: FlatEnvelope '[Err1, Err2] FooBar -> IO () + handleEnvelope (FlatEnvelope env) = + putStrLn $ + catchesEnvelope + ( \Err1 -> "Failure: Err1" + , \Err2 -> "Failure: Err2" + ) + (\(FooBar foobar) -> "Success: " <> show foobar) + env + handleServantError servantErr = putStrLn $ "Got a ServantErr: " <> show servantErr + +-- | This function uses the 'barClient' function to send a 'Bar' to +-- the server. +-- +-- Note that we can just use 'emptyEnvelope' to get the result, as we know there +-- are no errors. +-- Also the above fix is not necessary, as we don't have errors. +runBar :: ClientEnv -> Bar -> IO () +runBar clientEnv bar = do + eitherRes <- runClientM (barClient bar) clientEnv + case eitherRes of + Left servantErr -> putStrLn $ "Got a ServantErr: " <> show servantErr + Right (FlatEnvelope env) -> do + let (FooBar foobar) = emptyEnvelope env + putStrLn $ "Success: " <> show foobar + +-- | Run 'runFoo' or 'runBar' depending on the given CLI argument +runClient :: ClientEnv -> String -> IO () +runClient clientEnv query | query == "Bar" || query == "bar" + = runBar clientEnv Bar +runClient clientEnv s + = case readMaybe s of + Just n -> runFoo clientEnv $ Foo n + Nothing -> putStrLn $ "Not a valid query: " <> s + +-- | Run the client +clientMain :: IO () +clientMain = do + manager <- newManager defaultManagerSettings + args <- getArgs + query <- case args of + q : _ -> pure q + _ -> putStrLn "No args given, using 'bar'"$> "bar" + let clientEnv = ClientEnv manager baseUrl Nothing + runClient clientEnv query + +baseUrl :: BaseUrl +baseUrl = BaseUrl Http "localhost" port "" + +---------- +-- Main -- +---------- + +-- | Run everything: +-- 1. generate and print documentation +-- 2. start the server forked +-- 3. run the client +main :: IO () +main = do + putStrLn . markdown . docs $ Proxy @Api + _ <- forkIO serverMain + clientMain + +-- | The port to run the server on. +port :: Int +port = 8201 diff --git a/servant-checked-exceptions/servant-checked-exceptions.cabal b/servant-checked-exceptions/servant-checked-exceptions.cabal index 5a0ebda..9d5fa1e 100644 --- a/servant-checked-exceptions/servant-checked-exceptions.cabal +++ b/servant-checked-exceptions/servant-checked-exceptions.cabal @@ -107,6 +107,32 @@ executable servant-checked-exceptions-example-envelopet else buildable: False +executable servant-checked-exceptions-example-customenvelope + main-is: CustomEnvelope.hs + other-modules: Api + hs-source-dirs: example + build-depends: base + , aeson + , http-api-data + , http-client + , http-types + , optparse-applicative + , servant + , servant-checked-exceptions + , servant-client >= 0.13 + , servant-docs + , servant-server + , text + , wai + , warp + default-language: Haskell2010 + ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N + + if flag(buildexample) + buildable: True + else + buildable: False + test-suite servant-checked-exceptions-test type: exitcode-stdio-1.0 main-is: Spec.hs diff --git a/servant-checked-exceptions/src/Servant/Checked/Exceptions/Internal/Servant/Client.hs b/servant-checked-exceptions/src/Servant/Checked/Exceptions/Internal/Servant/Client.hs index 89df6c5..2793903 100644 --- a/servant-checked-exceptions/src/Servant/Checked/Exceptions/Internal/Servant/Client.hs +++ b/servant-checked-exceptions/src/Servant/Checked/Exceptions/Internal/Servant/Client.hs @@ -32,185 +32,184 @@ import Servant.API (Verb, (:>), (:<|>)) import Servant.Client (HasClient(clientWithRoute, Client)) import Servant.Client.Core -import Servant.Checked.Exceptions.Internal.Envelope (Envelope) import Servant.Checked.Exceptions.Internal.Servant.API - (NoThrow, Throws, Throwing, ThrowingNonterminal) + (NoThrow', Throws', Throwing', ThrowingNonterminal) -- TODO: Make sure to also account for when headers are being used. -- | Change a 'Throws' into 'Throwing'. -instance (RunClient m, HasClient m (Throwing '[e] :> api)) => HasClient m (Throws e :> api) where - type Client m (Throws e :> api) = Client m (Throwing '[e] :> api) +instance (RunClient m, HasClient m (Throwing' envel '[e] :> api)) => HasClient m (Throws' envel e :> api) where + type Client m (Throws' envel e :> api) = Client m (Throwing' envel '[e] :> api) clientWithRoute :: Proxy m - -> Proxy (Throws e :> api) + -> Proxy (Throws' envel e :> api) -> Request - -> Client m (Throwing '[e] :> api) - clientWithRoute p Proxy = clientWithRoute p (Proxy @(Throwing '[e] :> api)) + -> Client m (Throwing' envel '[e] :> api) + clientWithRoute p Proxy = clientWithRoute p (Proxy @(Throwing' envel '[e] :> api)) hoistClientMonad :: Proxy m - -> Proxy (Throws e :> api) + -> Proxy (Throws' envel e :> api) -> (forall x. mon x -> mon' x) - -> Client mon (Throws e :> api) - -> Client mon' (Throwing '[e] :> api) - hoistClientMonad pm _ = hoistClientMonad pm (Proxy @(Throwing '[e] :> api)) + -> Client mon (Throws' envel e :> api) + -> Client mon' (Throwing' envel '[e] :> api) + hoistClientMonad pm _ = hoistClientMonad pm (Proxy @(Throwing' envel '[e] :> api)) -- | When @'Throwing' es@ comes before a 'Verb', change it into the same 'Verb' -- but returning an @'Envelope' es@. -instance (HasClient m (Verb method status ctypes (Envelope es a))) => - HasClient m (Throwing es :> Verb method status ctypes a) where +instance (HasClient m (Verb method status ctypes (envel es a))) => + HasClient m (Throwing' envel es :> Verb method status ctypes a) where - type Client m (Throwing es :> Verb method status ctypes a) = - Client m (Verb method status ctypes (Envelope es a)) + type Client m (Throwing' envel es :> Verb method status ctypes a) = + Client m (Verb method status ctypes (envel es a)) clientWithRoute :: Proxy m - -> Proxy (Throwing es :> Verb method status ctypes a) + -> Proxy (Throwing' envel es :> Verb method status ctypes a) -> Request - -> Client m (Verb method status ctypes (Envelope es a)) + -> Client m (Verb method status ctypes (envel es a)) clientWithRoute p Proxy = - clientWithRoute p (Proxy :: Proxy (Verb method status ctypes (Envelope es a))) + clientWithRoute p (Proxy :: Proxy (Verb method status ctypes (envel es a))) hoistClientMonad :: Proxy m - -> Proxy (Throwing es :> Verb method status ctypes a) + -> Proxy (Throwing' envel es :> Verb method status ctypes a) -> (forall x. mon x -> mon' x) - -> Client mon (Throwing es :> Verb method status ctypes a) - -> Client mon' (Verb method status ctypes (Envelope es a)) + -> Client mon (Throwing' envel es :> Verb method status ctypes a) + -> Client mon' (Verb method status ctypes (envel es a)) hoistClientMonad pm _ = - hoistClientMonad pm (Proxy @(Verb method status ctypes (Envelope es a))) + hoistClientMonad pm (Proxy @(Verb method status ctypes (envel es a))) -- | When 'NoThrow' comes before a 'Verb', change it into the same 'Verb' -- but returning an @'Envelope' \'[]@. -instance (RunClient m, HasClient m (Verb method status ctypes (Envelope '[] a))) => - HasClient m (NoThrow :> Verb method status ctypes a) where +instance (RunClient m, HasClient m (Verb method status ctypes (envel '[] a))) => + HasClient m (NoThrow' envel :> Verb method status ctypes a) where - type Client m (NoThrow :> Verb method status ctypes a) = - Client m (Verb method status ctypes (Envelope '[] a)) + type Client m (NoThrow' envel :> Verb method status ctypes a) = + Client m (Verb method status ctypes (envel '[] a)) clientWithRoute :: Proxy m - -> Proxy (NoThrow :> Verb method status ctypes a) + -> Proxy (NoThrow' envel :> Verb method status ctypes a) -> Request - -> Client m (Verb method status ctypes (Envelope '[] a)) + -> Client m (Verb method status ctypes (envel '[] a)) clientWithRoute p Proxy = - clientWithRoute p (Proxy :: Proxy (Verb method status ctypes (Envelope '[] a))) + clientWithRoute p (Proxy :: Proxy (Verb method status ctypes (envel '[] a))) hoistClientMonad :: Proxy m - -> Proxy (NoThrow :> Verb method status ctypes a) + -> Proxy (NoThrow' envel :> Verb method status ctypes a) -> (forall x. mon x -> mon' x) - -> Client mon (NoThrow :> Verb method status ctypes a) - -> Client mon' (Verb method status ctypes (Envelope '[] a)) + -> Client mon (NoThrow' envel :> Verb method status ctypes a) + -> Client mon' (Verb method status ctypes (envel '[] a)) hoistClientMonad pm _ = - hoistClientMonad pm (Proxy @(Verb method status ctypes (Envelope '[] a))) + hoistClientMonad pm (Proxy @(Verb method status ctypes (envel '[] a))) -- | When @'Throwing' es@ comes before ':<|>', push @'Throwing' es@ into each -- branch of the API. -instance (RunClient m, HasClient m ((Throwing es :> api1) :<|> (Throwing es :> api2))) => - HasClient m (Throwing es :> (api1 :<|> api2)) where +instance (RunClient m, HasClient m ((Throwing' envel es :> api1) :<|> (Throwing' envel es :> api2))) => + HasClient m (Throwing' envel es :> (api1 :<|> api2)) where - type Client m (Throwing es :> (api1 :<|> api2)) = - Client m ((Throwing es :> api1) :<|> (Throwing es :> api2)) + type Client m (Throwing' envel es :> (api1 :<|> api2)) = + Client m ((Throwing' envel es :> api1) :<|> (Throwing' envel es :> api2)) clientWithRoute :: Proxy m - -> Proxy (Throwing es :> (api1 :<|> api2)) + -> Proxy (Throwing' envel es :> (api1 :<|> api2)) -> Request - -> Client m ((Throwing es :> api1) :<|> (Throwing es :> api2)) + -> Client m ((Throwing' envel es :> api1) :<|> (Throwing' envel es :> api2)) clientWithRoute p _ = - clientWithRoute p (Proxy :: Proxy ((Throwing es :> api1) :<|> (Throwing es :> api2))) + clientWithRoute p (Proxy :: Proxy ((Throwing' envel es :> api1) :<|> (Throwing' envel es :> api2))) hoistClientMonad :: Proxy m - -> Proxy (Throwing es :> (api1 :<|> api2)) + -> Proxy (Throwing' envel es :> (api1 :<|> api2)) -> (forall x. mon x -> mon' x) - -> Client mon (Throwing es :> (api1 :<|> api2)) - -> Client mon' ((Throwing es :> api1) :<|> (Throwing es :> api2)) + -> Client mon (Throwing' envel es :> (api1 :<|> api2)) + -> Client mon' ((Throwing' envel es :> api1) :<|> (Throwing' envel es :> api2)) hoistClientMonad pm _ = - hoistClientMonad pm (Proxy @(Throwing es :> (api1 :<|> api2))) + hoistClientMonad pm (Proxy @(Throwing' envel es :> (api1 :<|> api2))) -- | When 'NoThrow' comes before ':<|>', push 'NoThrow' into each branch of the -- API. -instance (RunClient m, HasClient m ((NoThrow :> api1) :<|> (NoThrow :> api2))) => - HasClient m (NoThrow :> (api1 :<|> api2)) where +instance (RunClient m, HasClient m ((NoThrow' envel :> api1) :<|> (NoThrow' envel :> api2))) => + HasClient m (NoThrow' envel :> (api1 :<|> api2)) where - type Client m (NoThrow :> (api1 :<|> api2)) = - Client m ((NoThrow :> api1) :<|> (NoThrow :> api2)) + type Client m (NoThrow' envel :> (api1 :<|> api2)) = + Client m ((NoThrow' envel :> api1) :<|> (NoThrow' envel :> api2)) clientWithRoute :: Proxy m - -> Proxy (NoThrow :> (api1 :<|> api2)) + -> Proxy (NoThrow' envel :> (api1 :<|> api2)) -> Request - -> Client m ((NoThrow :> api1) :<|> (NoThrow :> api2)) + -> Client m ((NoThrow' envel :> api1) :<|> (NoThrow' envel :> api2)) clientWithRoute p _ = - clientWithRoute p (Proxy :: Proxy ((NoThrow :> api1) :<|> (NoThrow :> api2))) + clientWithRoute p (Proxy :: Proxy ((NoThrow' envel :> api1) :<|> (NoThrow' envel :> api2))) hoistClientMonad :: Proxy m - -> Proxy (NoThrow :> (api1 :<|> api2)) + -> Proxy (NoThrow' envel :> (api1 :<|> api2)) -> (forall x. mon x -> mon' x) - -> Client mon (NoThrow :> (api1 :<|> api2)) - -> Client mon' ((NoThrow :> api1) :<|> (NoThrow :> api2)) + -> Client mon (NoThrow' envel :> (api1 :<|> api2)) + -> Client mon' ((NoThrow' envel :> api1) :<|> (NoThrow' envel :> api2)) hoistClientMonad pm _ = - hoistClientMonad pm (Proxy @(NoThrow :> (api1 :<|> api2))) + hoistClientMonad pm (Proxy @(NoThrow' envel :> (api1 :<|> api2))) -- | When a @'Throws' e@ comes immediately after a @'Throwing' es@, 'Snoc' the -- @e@ onto the @es@. Otherwise, if @'Throws' e@ comes before any other -- combinator, push it down so it is closer to the 'Verb'. -instance (RunClient m, HasClient m (ThrowingNonterminal (Throwing es :> api :> apis))) => - HasClient m (Throwing es :> api :> apis) where +instance (RunClient m, HasClient m (ThrowingNonterminal (Throwing' envel es :> api :> apis))) => + HasClient m (Throwing' envel es :> api :> apis) where - type Client m (Throwing es :> api :> apis) = - Client m (ThrowingNonterminal (Throwing es :> api :> apis)) + type Client m (Throwing' envel es :> api :> apis) = + Client m (ThrowingNonterminal (Throwing' envel es :> api :> apis)) clientWithRoute :: Proxy m - -> Proxy (Throwing es :> api :> apis) + -> Proxy (Throwing' envel es :> api :> apis) -> Request - -> Client m (ThrowingNonterminal (Throwing es :> api :> apis)) + -> Client m (ThrowingNonterminal (Throwing' envel es :> api :> apis)) clientWithRoute p _ = - clientWithRoute p (Proxy :: Proxy (ThrowingNonterminal (Throwing es :> api :> apis))) + clientWithRoute p (Proxy :: Proxy (ThrowingNonterminal (Throwing' envel es :> api :> apis))) hoistClientMonad :: Proxy m - -> Proxy (Throwing es :> api :> apis) + -> Proxy (Throwing' envel es :> api :> apis) -> (forall x. mon x -> mon' x) - -> Client mon (Throwing es :> api :> apis) - -> Client mon' (ThrowingNonterminal (Throwing es :> api :> apis)) + -> Client mon (Throwing' envel es :> api :> apis) + -> Client mon' (ThrowingNonterminal (Throwing' envel es :> api :> apis)) hoistClientMonad pm _ = - hoistClientMonad pm (Proxy @(ThrowingNonterminal (Throwing es :> api :> apis))) + hoistClientMonad pm (Proxy @(ThrowingNonterminal (Throwing' envel es :> api :> apis))) -- | When 'NoThrow' comes before any other combinator, push it down so it is -- closer to the 'Verb'. -instance (RunClient m, HasClient m (api :> NoThrow :> apis)) => - HasClient m (NoThrow :> api :> apis) where +instance (RunClient m, HasClient m (api :> NoThrow' envel :> apis)) => + HasClient m (NoThrow' envel :> api :> apis) where - type Client m (NoThrow :> api :> apis) = - Client m (api :> NoThrow :> apis) + type Client m (NoThrow' envel :> api :> apis) = + Client m (api :> NoThrow' envel :> apis) clientWithRoute :: Proxy m - -> Proxy (NoThrow :> api :> apis) + -> Proxy (NoThrow' envel :> api :> apis) -> Request - -> Client m (api :> NoThrow :> apis) + -> Client m (api :> NoThrow' envel :> apis) clientWithRoute p _ = - clientWithRoute p (Proxy :: Proxy (api :> NoThrow :> apis)) + clientWithRoute p (Proxy :: Proxy (api :> NoThrow' envel :> apis)) hoistClientMonad :: Proxy m - -> Proxy (NoThrow :> api :> apis) + -> Proxy (NoThrow' envel :> api :> apis) -> (forall x. mon x -> mon' x) - -> Client mon (NoThrow :> api :> apis) - -> Client mon' (api :> NoThrow :> apis) + -> Client mon (NoThrow' envel :> api :> apis) + -> Client mon' (api :> NoThrow' envel :> apis) hoistClientMonad pm _ = - hoistClientMonad pm (Proxy @(api :> NoThrow :> apis)) + hoistClientMonad pm (Proxy @(api :> NoThrow' envel :> apis)) diff --git a/servant-checked-exceptions/src/Servant/Checked/Exceptions/Internal/Servant/Server.hs b/servant-checked-exceptions/src/Servant/Checked/Exceptions/Internal/Servant/Server.hs index d7cbadb..abe5330 100644 --- a/servant-checked-exceptions/src/Servant/Checked/Exceptions/Internal/Servant/Server.hs +++ b/servant-checked-exceptions/src/Servant/Checked/Exceptions/Internal/Servant/Server.hs @@ -29,10 +29,8 @@ module Servant.Checked.Exceptions.Internal.Servant.Server where import Data.ByteString (ByteString) import qualified Data.ByteString.Lazy as LBS -import Data.Functor.Identity import Data.Maybe import Data.Proxy (Proxy(Proxy)) -import Data.WorldPeace (OpenUnion, Union(That, This)) import GHC.TypeLits (KnownNat, natVal) import Network.HTTP.Types import Network.Wai @@ -67,151 +65,150 @@ import Servant , reflectMethod ) -import Servant.Checked.Exceptions.Internal.Envelope (Envelope, envelope) import Servant.Checked.Exceptions.Internal.Servant.API ( AllErrStatus - , ErrStatus(toErrStatus) - , NoThrow - , Throwing + , EnvelopeStatus(..) + , NoThrow' + , Throwing' , ThrowingNonterminal - , Throws + , Throws' ) -import Servant.Checked.Exceptions.Verbs (VerbWithErr) +import Servant.Checked.Exceptions.Verbs (VerbWithErr') -- TODO: Make sure to also account for when headers are being used. -- This might be hard to do: -- https://github.com/cdepillabout/servant-checked-exceptions/issues/4 -- | Change a 'Throws' into 'Throwing'. -instance (HasServer (Throwing '[e] :> api) context) => - HasServer (Throws e :> api) context where +instance (HasServer (Throwing' envel '[e] :> api) context) => + HasServer (Throws' envel e :> api) context where - type ServerT (Throws e :> api) m = - ServerT (Throwing '[e] :> api) m + type ServerT (Throws' envel e :> api) m = + ServerT (Throwing' envel '[e] :> api) m hoistServerWithContext _ = - hoistServerWithContext (Proxy :: Proxy (Throwing '[e] :> api)) + hoistServerWithContext (Proxy :: Proxy (Throwing' envel '[e] :> api)) route - :: Proxy (Throws e :> api) + :: Proxy (Throws' envel e :> api) -> Context context - -> Delayed env (ServerT (Throwing '[e] :> api) Handler) + -> Delayed env (ServerT (Throwing' envel '[e] :> api) Handler) -> Router env - route _ = route (Proxy :: Proxy (Throwing '[e] :> api)) + route _ = route (Proxy :: Proxy (Throwing' envel '[e] :> api)) -- | When @'Throwing' es@ comes before a 'Verb', change it into the same 'Verb' -- but returning an @'Envelope' es@. -instance (HasServer (VerbWithErr method status ctypes es a) context) => - HasServer (Throwing es :> Verb method status ctypes a) context where +instance (HasServer (VerbWithErr' method status ctypes envel es a) context) => + HasServer (Throwing' envel es :> Verb method status ctypes a) context where - type ServerT (Throwing es :> Verb method status ctypes a) m = - ServerT (VerbWithErr method status ctypes es a) m + type ServerT (Throwing' envel es :> Verb method status ctypes a) m = + ServerT (VerbWithErr' method status ctypes envel es a) m hoistServerWithContext _ = - hoistServerWithContext (Proxy :: Proxy (VerbWithErr method status ctypes es a)) + hoistServerWithContext (Proxy :: Proxy (VerbWithErr' method status ctypes envel es a)) route - :: Proxy (Throwing es :> Verb method status ctypes a) + :: Proxy (Throwing' envel es :> Verb method status ctypes a) -> Context context -> Delayed env - (ServerT (VerbWithErr method status ctypes es a) Handler) + (ServerT (VerbWithErr' method status ctypes envel es a) Handler) -> Router env route _ = route - (Proxy :: Proxy (VerbWithErr method status ctypes es a)) + (Proxy :: Proxy (VerbWithErr' method status ctypes envel es a)) -- | When 'NoThrow' comes before a 'Verb', change it into the same 'Verb' -- but returning an @'Envelope' \'[]@. instance - ( HasServer (VerbWithErr method status ctypes '[] a) context + ( HasServer (VerbWithErr' method status ctypes envel '[] a) context ) => - HasServer (NoThrow :> Verb method status ctypes a) context where + HasServer (NoThrow' envel :> Verb method status ctypes a) context where - type ServerT (NoThrow :> Verb method status ctypes a) m = - ServerT (VerbWithErr method status ctypes '[] a) m + type ServerT (NoThrow' envel :> Verb method status ctypes a) m = + ServerT (VerbWithErr' method status ctypes envel '[] a) m hoistServerWithContext _ = - hoistServerWithContext (Proxy :: Proxy (VerbWithErr method status ctypes '[] a)) + hoistServerWithContext (Proxy :: Proxy (VerbWithErr' method status ctypes envel '[] a)) route - :: Proxy (NoThrow :> Verb method status ctypes a) + :: Proxy (NoThrow' envel :> Verb method status ctypes a) -> Context context - -> Delayed env (ServerT (VerbWithErr method status ctypes '[] a) Handler) + -> Delayed env (ServerT (VerbWithErr' method status ctypes envel '[] a) Handler) -> Router env - route _ = route (Proxy :: Proxy (VerbWithErr method status ctypes '[] a)) + route _ = route (Proxy :: Proxy (VerbWithErr' method status ctypes envel '[] a)) -- | When @'Throwing' es@ comes before ':<|>', push @'Throwing' es@ into each -- branch of the API. -instance HasServer ((Throwing es :> api1) :<|> (Throwing es :> api2)) context => - HasServer (Throwing es :> (api1 :<|> api2)) context where +instance HasServer ((Throwing' envel es :> api1) :<|> (Throwing' envel es :> api2)) context => + HasServer (Throwing' envel es :> (api1 :<|> api2)) context where - type ServerT (Throwing es :> (api1 :<|> api2)) m = - ServerT ((Throwing es :> api1) :<|> (Throwing es :> api2)) m + type ServerT (Throwing' envel es :> (api1 :<|> api2)) m = + ServerT ((Throwing' envel es :> api1) :<|> (Throwing' envel es :> api2)) m hoistServerWithContext _ = - hoistServerWithContext (Proxy :: Proxy ((Throwing es :> api1) :<|> (Throwing es :> api2))) + hoistServerWithContext (Proxy :: Proxy ((Throwing' envel es :> api1) :<|> (Throwing' envel es :> api2))) route - :: Proxy (Throwing es :> (api1 :<|> api2)) + :: Proxy (Throwing' envel es :> (api1 :<|> api2)) -> Context context - -> Delayed env (ServerT ((Throwing es :> api1) :<|> (Throwing es :> api2)) Handler) + -> Delayed env (ServerT ((Throwing' envel es :> api1) :<|> (Throwing' envel es :> api2)) Handler) -> Router env - route _ = route (Proxy :: Proxy ((Throwing es :> api1) :<|> (Throwing es :> api2))) + route _ = route (Proxy :: Proxy ((Throwing' envel es :> api1) :<|> (Throwing' envel es :> api2))) -- | When 'NoThrow' comes before ':<|>', push 'NoThrow' into each -- branch of the API. -instance HasServer ((NoThrow :> api1) :<|> (NoThrow :> api2)) context => - HasServer (NoThrow :> (api1 :<|> api2)) context where +instance HasServer ((NoThrow' envel :> api1) :<|> (NoThrow' envel :> api2)) context => + HasServer (NoThrow' envel :> (api1 :<|> api2)) context where - type ServerT (NoThrow :> (api1 :<|> api2)) m = - ServerT ((NoThrow :> api1) :<|> (NoThrow :> api2)) m + type ServerT (NoThrow' envel :> (api1 :<|> api2)) m = + ServerT ((NoThrow' envel :> api1) :<|> (NoThrow' envel :> api2)) m hoistServerWithContext _ = - hoistServerWithContext (Proxy :: Proxy ((NoThrow :> api1) :<|> (NoThrow :> api2))) + hoistServerWithContext (Proxy :: Proxy ((NoThrow' envel :> api1) :<|> (NoThrow' envel :> api2))) route - :: Proxy (NoThrow :> (api1 :<|> api2)) + :: Proxy (NoThrow' envel :> (api1 :<|> api2)) -> Context context - -> Delayed env (ServerT ((NoThrow :> api1) :<|> (NoThrow :> api2)) Handler) + -> Delayed env (ServerT ((NoThrow' envel :> api1) :<|> (NoThrow' envel :> api2)) Handler) -> Router env - route _ = route (Proxy :: Proxy ((NoThrow :> api1) :<|> (NoThrow :> api2))) + route _ = route (Proxy :: Proxy ((NoThrow' envel :> api1) :<|> (NoThrow' envel :> api2))) -- | When a @'Throws' e@ comes immediately after a @'Throwing' es@, 'Snoc' the -- @e@ onto the @es@. Otherwise, if @'Throws' e@ comes before any other -- combinator, push it down so it is closer to the 'Verb'. -instance HasServer (ThrowingNonterminal (Throwing es :> api :> apis)) context => - HasServer (Throwing es :> api :> apis) context where +instance HasServer (ThrowingNonterminal (Throwing' envel es :> api :> apis)) context => + HasServer (Throwing' envel es :> api :> apis) context where - type ServerT (Throwing es :> api :> apis) m = - ServerT (ThrowingNonterminal (Throwing es :> api :> apis)) m + type ServerT (Throwing' envel es :> api :> apis) m = + ServerT (ThrowingNonterminal (Throwing' envel es :> api :> apis)) m hoistServerWithContext _ = - hoistServerWithContext (Proxy :: Proxy (ThrowingNonterminal (Throwing es :> api :> apis))) + hoistServerWithContext (Proxy :: Proxy (ThrowingNonterminal (Throwing' envel es :> api :> apis))) route - :: Proxy (Throwing es :> api :> apis) + :: Proxy (Throwing' envel es :> api :> apis) -> Context context - -> Delayed env (ServerT (ThrowingNonterminal (Throwing es :> api :> apis)) Handler) + -> Delayed env (ServerT (ThrowingNonterminal (Throwing' envel es :> api :> apis)) Handler) -> Router env - route _ = route (Proxy :: Proxy (ThrowingNonterminal (Throwing es :> api :> apis))) + route _ = route (Proxy :: Proxy (ThrowingNonterminal (Throwing' envel es :> api :> apis))) -- | When 'NoThrow' comes before any combinator, push it down so it is closer -- to the 'Verb'. -instance HasServer (api :> NoThrow :> apis) context => - HasServer (NoThrow :> api :> apis) context where +instance HasServer (api :> NoThrow' envel :> apis) context => + HasServer (NoThrow' envel :> api :> apis) context where - type ServerT (NoThrow :> api :> apis) m = - ServerT (api :> NoThrow :> apis) m + type ServerT (NoThrow' envel :> api :> apis) m = + ServerT (api :> NoThrow' envel :> apis) m hoistServerWithContext _ = - hoistServerWithContext (Proxy :: Proxy (api :> NoThrow :> apis)) + hoistServerWithContext (Proxy :: Proxy (api :> NoThrow' envel :> apis)) route - :: Proxy (NoThrow :> api :> apis) + :: Proxy (NoThrow' envel :> api :> apis) -> Context context - -> Delayed env (ServerT (api :> NoThrow :> apis) Handler) + -> Delayed env (ServerT (api :> NoThrow' envel :> apis) Handler) -> Router env - route _ = route (Proxy :: Proxy (api :> NoThrow :> apis)) + route _ = route (Proxy :: Proxy (api :> NoThrow' envel :> apis)) --------------------- -- Verb With Error -- @@ -219,22 +216,23 @@ instance HasServer (api :> NoThrow :> apis) context => instance {-# OVERLAPPABLE #-} - ( AllCTRender ctypes (Envelope es a) + ( AllCTRender ctypes (envel es a) , AllErrStatus es , KnownNat successStatus , ReflectMethod method + , EnvelopeStatus es envel ) => - HasServer (VerbWithErr method successStatus ctypes es a) context where + HasServer (VerbWithErr' method successStatus ctypes envel es a) context where - type ServerT (VerbWithErr method successStatus ctypes es a) m = - m (Envelope es a) + type ServerT (VerbWithErr' method successStatus ctypes envel es a) m = + m (envel es a) hoistServerWithContext _ _ nt = nt route - :: Proxy (VerbWithErr method successStatus ctypes es a) + :: Proxy (VerbWithErr' method successStatus ctypes envel es a) -> Context context - -> Delayed env (Handler (Envelope es a)) + -> Delayed env (Handler (envel es a)) -> Router' env ( Request -> (RouteResult Response -> IO ResponseReceived) -> @@ -250,12 +248,12 @@ instance toEnum . fromInteger $ natVal (Proxy :: Proxy successStatus) methodRouter :: - forall ctypes a es env. - (AllCTRender ctypes (Envelope es a), AllErrStatus es) + forall ctypes a envel es env. + (AllCTRender ctypes (envel es a), AllErrStatus es, EnvelopeStatus es envel) => Method -> Status -> Proxy ctypes - -> Delayed env (Handler (Envelope es a)) + -> Delayed env (Handler (envel es a)) -> Router' env ( Request -> (RouteResult Response -> IO ResponseReceived)-> @@ -276,9 +274,9 @@ methodRouter method successStatus proxy action = leafRouter route' `addAcceptCheck` acceptCheck proxy accH runAction theAction env request respond $ go request accH - go :: Request -> ByteString -> Envelope es a -> RouteResult Response + go :: Request -> ByteString -> envel es a -> RouteResult Response go request accH envel = do - let status = envelope getErrStatus (const successStatus) envel + let status = getEnvelopeStatus envel successStatus let handleA = handleAcceptH proxy (AcceptHeader accH) envel processMethodRouter handleA status method Nothing request @@ -300,10 +298,6 @@ acceptCheck proxy accH | canHandleAcceptH proxy (AcceptHeader accH) = return () | otherwise = delayedFail err406 -getErrStatus :: AllErrStatus es => OpenUnion es -> Status -getErrStatus (This (Identity e)) = toErrStatus e -getErrStatus (That es) = getErrStatus es - processMethodRouter :: Maybe (LBS.ByteString, LBS.ByteString) -> Status