Skip to content

union verbs #1314

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 42 commits into from
Oct 31, 2020
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
936c29b
UVerb: initial version.
fisx Jun 16, 2020
f2725bd
Use Servant.API.Status.
fisx Jun 16, 2020
d743a5f
Fixup
fisx Jun 16, 2020
b3a6124
Move default extensions to modules again.
fisx Jun 19, 2020
1ef5a85
Add useful TOJSON instances for HasStatus.
fisx Jun 30, 2020
254cf71
UVerb cookbook chapter.
fisx Jun 30, 2020
109bdf1
Related work; haddocks.
fisx Jun 30, 2020
3474edd
Section on idiomatic exceptions in uverb cookbook.
maksbotan Jun 30, 2020
cb0bdbd
Fix cookbook.
fisx Jul 2, 2020
a9f61fa
nit-picks.
fisx Jul 2, 2020
28cd056
Transform Main.hs into Tests (#1)
voidus Jul 23, 2020
ad2cb24
Move instance HasClient UVerb to a better place.
fisx Jul 31, 2020
b14e69a
Fix trailing whitespace.
fisx Jul 31, 2020
1901840
Improve types & haddocks of uverb client helpers.
fisx Jul 31, 2020
12f2ea5
Fix re-exports.
fisx Jul 31, 2020
33073a6
Fix warnings.
fisx Jul 31, 2020
9980b84
Fix type error.
fisx Jul 31, 2020
1c71456
Fix deprecation warning (unrelated, but relentlessly irritating).
fisx Jul 31, 2020
ba3bed6
Activate/tune failing test case.
fisx Aug 9, 2020
392b77f
"Fix" TODOs.
fisx Aug 9, 2020
ef372d9
Fix status code handling for servant-client.
fisx Aug 9, 2020
cd5f624
Tweaks
fisx Aug 9, 2020
a2f3e3b
Fix servant-http-streams, streaming clients.
fisx Aug 9, 2020
0c0c27f
Accomodate ghc80.
fisx Aug 9, 2020
a8734ea
Move general-purpose instances to production code.
fisx Aug 11, 2020
8a413c2
Remove redundant code.
fisx Aug 11, 2020
f34df7c
Remove redundant code.
fisx Aug 11, 2020
6c53469
Use I, not Identity, this is more idiomatic in SOP.
fisx Oct 15, 2020
cb008f2
Better implementation of extractUResp.
fisx Oct 15, 2020
7c16739
Better implementation of collapseUResp.
fisx Oct 15, 2020
95f29b1
Remove unused imports.
fisx Oct 15, 2020
a024e0b
Haddocks.
fisx Oct 17, 2020
d55b698
Fix cookbook-uverb compilation
maksbotan Oct 15, 2020
1223924
Simplify OpenUnion after @arianvp 's idea
maksbotan Oct 15, 2020
c77242c
Remove dead code.
fisx Oct 17, 2020
b9be0e4
Re-order top-level definitions.
fisx Oct 17, 2020
a97b332
Refactor.
fisx Oct 17, 2020
5a9875c
Haddocks.
fisx Oct 17, 2020
8da6962
Fix unused imports.
fisx Oct 17, 2020
9d3430f
Copyright header for module copied from world-peace.
fisx Oct 25, 2020
d631d54
Cleanup union types interface.
fisx Oct 25, 2020
217e31d
Cleanup union types interface.
fisx Oct 25, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions cabal.project
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,16 @@ packages:
doc/cookbook/custom-errors
doc/cookbook/basic-streaming
doc/cookbook/db-postgres-pool
-- doc/cookbook/db-sqlite-simple
-- doc/cookbook/db-sqlite-simple
doc/cookbook/file-upload
doc/cookbook/generic
-- doc/cookbook/hoist-server-with-context
-- doc/cookbook/https
-- doc/cookbook/https
-- doc/cookbook/jwt-and-basic-auth/
doc/cookbook/pagination
-- doc/cookbook/sentry
doc/cookbook/testing
doc/cookbook/uverb
doc/cookbook/structuring-apis
doc/cookbook/using-custom-monad
doc/cookbook/using-free-client
Expand Down
206 changes: 206 additions & 0 deletions doc/cookbook/uverb/UVerb.lhs
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
# Listing alternative responses and exceptions in your API types

Servant allows you to talk about the exceptions you throw in your API
types. This is not limited to actual exceptions, you can write
handlers that respond with arbitrary open unions of types.

## Preliminaries

```haskell
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -Wall -Wno-orphans #-}

import Control.Concurrent (threadDelay)
import Control.Concurrent.Async (async)
import Data.Aeson (FromJSON (..), ToJSON (..))
import Data.Aeson.Encode.Pretty (encodePretty)
import Data.String.Conversions (cs)
import Data.Swagger (ToSchema)
import Data.Typeable (Proxy (Proxy))
import qualified GHC.Generics as GHC
import qualified Network.HTTP.Client as Client
import qualified Network.Wai.Handler.Warp as Warp
import Servant.API
import Servant.API.UVerb
import Servant.Client
import Servant.Client.UVerb
import Servant.Server
import Servant.Server.UVerb
import Servant.Swagger
import Servant.Swagger.UVerb ()
```

## The API

This looks like a `Verb`-based routing table, except that `UVerb` has
no status, and carries a list of response types rather than a single
one. Each entry in the list carries its own response code.

```haskell
type API =
"fisx" :> Capture "bool" Bool
:> UVerb 'GET '[JSON] '[FisxUser, WithStatus 303 String]
:<|> "arian"
:> UVerb 'GET '[JSON] '[WithStatus 201 ArianUser]
```

Here are the details:

```haskell
data FisxUser = FisxUser {name :: String}
deriving (Eq, Show, GHC.Generic)

instance ToJSON FisxUser
instance FromJSON FisxUser
instance ToSchema FisxUser

-- | 'HasStatus' allows us to can get around 'WithStatus' if we want
-- to, and associate the status code with our resource types directly.
--
-- (To avoid orphan instances and make it more explicit what's in the
-- API and what isn't, we could even introduce a newtype 'Resource'
-- that wraps all the types we're using in our routing table, and then
-- define lots of 'HasStatus' instances for @Resource This@ and
-- @Resource That@.)
instance HasStatus FisxUser where
type StatusOf FisxUser = 203

data ArianUser = ArianUser
deriving (Eq, Show, GHC.Generic)

instance ToJSON ArianUser
instance FromJSON ArianUser
instance ToSchema ArianUser
```

## Server, Client, Swagger

You can just respond with any of the elements of the union in handlers.

```haskell
fisx :: Bool -> Handler (Union '[FisxUser, WithStatus 303 String])
fisx True = respond (FisxUser "fisx")
fisx False = respond (WithStatus @303 ("still fisx" :: String))

arian :: Handler (Union '[WithStatus 201 ArianUser])
arian = respond (WithStatus @201 ArianUser)
```

You can create client functions like you're used to:

```
fisxClient :: Bool -> ClientM (Union '[FisxUser, WithStatus 303 String])
arianClient :: ClientM (Union '[WithStatus 201 ArianUser])
(fisxClient :<|> arianClient) = client (Proxy @API)
```

... and that's basically it! Here are a few sample commands that
show you how the swagger docs look like and how you can handle the
result unions in clients:

```
main :: IO ()
main = do
putStrLn . cs . encodePretty $ toSwagger (Proxy @API)
_ <- async . Warp.run 8080 $ serve (Proxy @API) (fisx :<|> arian)
threadDelay 50000
mgr <- Client.newManager Client.defaultManagerSettings
let cenv = mkClientEnv mgr (BaseUrl Http "localhost" 8080 "")
result <- runClientM (fisxClient True) cenv
print $ collapseUResp (Proxy @Show) show <$> result
print $ extractUResp @FisxUser <$> result
print $ extractUResp @(WithStatus 303 String) <$> result
pure ()
```

## Idiomatic exceptions

Since `UVerb` (probably) will mostly be used for error-like responses, it may be desirable to be able to early abort handler, like with current servant one would use `throwError` with `ServerError`.

```haskell
newtype UVerbT xs m a = UVerbT { unUVerbT :: ExceptT (Union xs) m a }
deriving newtype (Functor, Applicative, Monad, MonadTrans)

-- | Deliberately hide 'ExceptT's 'MonadError' instance to be able to use
-- underlying monad's instance.
instance MonadError e m => MonadError e (UVerbT xs m) where
throwError = lift . throwError
catchError (UVerbT act) h = UVerbT $ ExceptT $
runExceptT act `catchError` (runExceptT . unUVerbT . h)

-- | This combinator runs 'UVerbT'. It applies 'respond' internally, so the handler
-- may use the usual 'return'.
runUVerbT :: (Monad m, HasStatus x, IsMember x xs) => UVerbT xs m x -> m (Union xs)
runUVerbT (UVerbT act) = either id id <$> runExceptT (act >>= respond)

-- | Short-circuit 'UVerbT' computation returning one of the response types.
throwUVerb :: (Monad m, HasStatus x, IsMember x xs) => x -> UVerbT xs m a
throwUVerb = UVerbT . ExceptT . fmap Left . respond
```

Example usage:

```haskell
h :: Handler (Union '[Foo, WithStatus 400 Bar])
h = runUVerbT $
when (something bad) $
throwUVerb $ WithStatus @400 Bar

when (really bad) $
throwError $ err500

-- a lot of code here...

return $ Foo 1 2 3
```

## Related Work

There is the [issue from
2017](https://github.com/haskell-servant/servant/issues/841) that was
resolved by the introduction of `UVerb`, with a long discussion on
alternative designs.

[servant-checked-exceptions](https://hackage.haskell.org/package/servant-checked-exceptions)
is a good solution to the problem, but it restricts the user to JSON
and a very specific envelop encoding for the union type, which is
often not acceptable. (One good reason for this design choice is that
it makes writing clients easier, where you need to get to the union
type from one representative, and you don't want to run several
parsers in the hope that the ones that should will always error out so
you can try until the right one returns a value.)

[servant-exceptions](https://github.com/ch1bo/servant-exceptions) is
another shot at at the problem. It is inspired by
servant-checked-exceptions, so it may be worth taking a closer look.
The README claims that
[cardano-sl](https://github.com/input-output-hk/cardano-sl) also has
some code for generalized error handling.

In an earier version of the `UVerb` implementation, we have used some
code from
[world-peace](https://hackage.haskell.org/package/world-peace), but
that package itself wasn't flexible enough, and we had to use
[sop-core](https://hackage.haskell.org/package/sop-core) to implement
the `HasServer` instance.

Here is a blog post we found on the subject:
https://lukwagoallan.com/posts/unifying-servant-server-error-responses

(If you have anything else, please add it here or let us know.)
32 changes: 32 additions & 0 deletions doc/cookbook/uverb/uverb.cabal
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: cookbook-uverb
version: 0.0.1
synopsis: How to use the 'UVerb' type.
description: Listing alternative responses and exceptions in your API types.
homepage: http://docs.servant.dev/
license: BSD3
license-file: ../../../servant/LICENSE
author: Servant Contributors
maintainer: [email protected]
category: Servant
build-type: Simple
cabal-version: >=1.10
tested-with: GHC==8.4.4, GHC==8.6.5, GHC==8.8.2

executable cookbook-testing
main-is: UVerb.lhs
build-depends: base == 4.*
, async
, aeson >= 1.2
, aeson-pretty >= 0.8.8
, string-conversions
, swagger2
, http-client
, warp
, wai
, servant
, servant-client
, servant-server
, servant-swagger
default-language: Haskell2010
ghc-options: -Wall -pgmL markdown-unlit
build-tool-depends: markdown-unlit:markdown-unlit
1 change: 1 addition & 0 deletions servant-client-core/servant-client-core.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ library
, http-types >= 0.12.2 && < 0.13
, network-uri >= 2.6.1.0 && < 2.7
, safe >= 0.3.17 && < 0.4
, sop-core >= 0.4.0.0 && < 0.6

hs-source-dirs: src
default-language: Haskell2010
Expand Down
2 changes: 2 additions & 0 deletions servant-client-core/src/Servant/Client/Core.hs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ module Servant.Client.Core
-- * Client generation
clientIn
, HasClient(..)
, collapseUResp
, extractUResp

-- * Request
, Request
Expand Down
Loading