Giter Site home page Giter Site logo

cdepillabout / servant-checked-exceptions Goto Github PK

View Code? Open in Web Editor NEW
71.0 7.0 14.0 249 KB

type-level errors for Servant APIs.

Home Page: https://hackage.haskell.org/package/servant-checked-exceptions

License: BSD 3-Clause "New" or "Revised" License

Makefile 1.44% Haskell 98.56%
haskell servant exceptions errors json server client docs hacktoberfest

servant-checked-exceptions's Introduction

Servant.Checked.Exceptions

Build Status Hackage Stackage LTS Stackage Nightly BSD3 license

servant-checked-exceptions provides a way to specify errors thrown by a Servant api on the type level. It allows easy composition between different error types.

servant-checked-exceptions provides the Throws data type to signify which errors can be thrown by an api. For instance, imagine a getAuthor api that returns an Author based on an AuthorId:

-- This is a servant-compatible type describing our api.
type Api =
  "author" :>
  Capture "author-id" AuthorId :>
  Throws DatabaseError :>
  Throws AuthorNotFoundError :>
  Get '[JSON] Author

-- These are the two errors that can be thrown:
data DatabaseError = DatabaseError
data AuthorNotFoundError = AuthorNotFoundError

The corresponding handler function uses the Envelope data type to model the possibility of returning an Author successfully, or either DatabaseError or AuthorNotFoundError unsuccessfully. Internally, Envelope is using an open sum-type to easily represent multiple different errors:

getAuthorHandler
  :: AuthorId
  -> Handler (Envelope '[DatabaseError, AuthorNotFoundError] Author)
getAuthorHandler authorId = ...

For more documentation and usage examples, see the documentation on Hackage.

Why would I want to use this?

Using Envelope with its open sum-type to represent errors gives us an easy way to reuse errors on multiple routes.

For instance, imagine that we had another api for updating an author's name, given the author's ID. Using Throws and Envelope, it might look like this:

type Api =
  "update-author-name" :>
  Capture "author-id" AuthorId :>
  Capture "author-name" AuthorName :>
  Throws DatabaseError :>
  Throws AuthorNotFoundError :>
  Throws AuthorNameTooShort :>
  Post '[JSON] Author

data AuthorNameTooShort = AuthorNameTooShort

postChangeAuthorName
  :: AuthorId
  -> AuthorName
  -> Handler (Envelope '[DatabaseError, AuthorNotFoundError, AuthorNameTooShort] Author)
postChangeAuthorName authorId newAuthorName = ...

We are able to reuse the DatabaseError and AuthorNotFoundError. If we try to return an error that is not declared using Throws, GHC will give us an error. We get flexiblity and type-safety.

When using servant-docs to create documentation, only one instance of ToSample needs to be created for each error (DatabaseError, AuthorNotFoundError, and AuthorNameTooShort). Multiple instances of ToSample do not need to be created for every different Envelope used in a handler.

Example

This repository contains an example of using servant-checked-exceptions. This includes an api, server, client, and documentation.

Below I show how to compile and run these examples.

Compile

The examples can be compiled by using the buildexample flag:

$ stack build --flag servant-checked-exceptions-core:buildexample --flag servant-checked-exceptions:buildexample

This creates three executables. A server, a client, and a documentaiton generator.

Run the server

The server is a small example that will take search queries and return results. The server can be run with the following command:

$ stack exec -- servant-checked-exceptions-example-server

This runs the server on port 8201. Here is an example of using curl to access the server. This will send the query hello:

$ curl \
    --request POST \
    --header 'Accept: application/json' \
    'http://localhost:8201/lax-search/hello'
{"data":"good"}

If you try to send a query that is not hello, the server will return an error:

$ curl \
    --request POST \
    --header 'Accept: application/json' \
    'http://localhost:8201/lax-search/goodbye'
{"err":"BadSearchTermErr"}

There is also a strict api, that requires hello to be capitalized like Hello:

$ curl \
    --request POST \
    --header 'Accept: application/json' \
    'http://localhost:8201/strict-search/hello'
{"err":"IncorrectCapitalization"}
$ curl \
    --request POST \
    --header 'Accept: application/json' \
    'http://localhost:8201/strict-search/Hello'
{"data":"good"}

Run the client

The client provides a small command line application to query the server. In order to use the client, the server must be running.

Use the client to access the lax search api:

$ stack exec -- servant-checked-exceptions-example-client foobar
the search term was not "Hello"
$ stack exec -- servant-checked-exceptions-example-client hello
Success: good

Use the client to access the strict search api:

$ stack exec -- servant-checked-exceptions-example-client --strict hello
the search term was not capitalized correctly
$ stack exec -- servant-checked-exceptions-example-client --strict Hello
Success: good

Run the documentation generator

The documentation generator will generate documentation for the api in Markdown:

$ stack exec -- servant-checked-exceptions-core-example-docs

Here is a small example of the documentation that will be generated for the lax search api:

## POST /lax-search/:query

#### Captures:

- *query*: a search string like "hello" or "bye"

#### Response:

- Status code 200
- Headers: []

- Supported content types are:

    - `application/json`

- This is a successful response.

{"data":"good"}

- a completely incorrect search term was used

{"err":"BadSearchTermErr"}

You can see that both the success and error responses are documented.

Packaging the core types

servant-checked-exceptions-core exports the core types need for building an API with checked exceptions, allowing you to avoid depending on server-side libraries like warp, Glob and servant-server. This can be useful if you are writing an API meant to be shared with ghcjs and run in a browser, where these dependencies aren't available.

Limitations

Currently, servant-client only treats HTTP responses as successful if they have a status code of 2XX. This means that any non-2XX errors thrown by servant-checked-exceptions don't get parsed into a typed Envelope as expected, but raised as a Servant ClientError. For more information, see issue #27.

Maintainers

  • Maintainer: cdepillabout
  • Maintainer: imalsogreg

servant-checked-exceptions's People

Contributors

bergmark avatar cdepillabout avatar dougburke avatar imalsogreg avatar lexi-lambda avatar luigy avatar mheinzel avatar schell avatar timofreiberg avatar zyla avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

servant-checked-exceptions's Issues

fix doctests on GHC-9

The doctests for servant-checked-exceptions-core appear to not work on GHC-9.

See commercialhaskell/stackage#6092 for an example of what this looks like.

This should be fixed so that doctests do successfully work on GHC-9.

servant-checked-exceptions-core should probably be moved to either use cabal-doctest or cabal-docspec.

NamedRoutes not implemented

I’m trying to use the servant-checked-exceptions package with NamedRoutes. But I want to add the "Throws" at the top of the api, for instance:

data ApiFoo mode = ApiFoo
  {
    get :: mode :- Get '[JSON] Foo
   , post :: mode :- "new" :> ReqBody '[JSON] Foo :> Post '[JSON] ()
  }

data ApiStore mode = ApiStore
  {
    fooStore :: mode :- "foo" :> NamedRoutes ApiFoo
  , barStore :: mode :- "bar" :> NamedRoutes ApiBar

  }

type API = Throws StoreError :> "store" :> NamedRoutes ApiStore

But Throws is not able to traverse NamedRoutes types. I tried to implement a HasServer (Throwing '[e] :> NamedRoutes api) instance, but I seem unable to do it correctly (I tried to find other packages that implement a similar instance too to use as example, in vain). I’d appreciate help for implementing that.

Thanks!

servant-client doesn't return envelopes for non-2xx status codes

Full reproduction: https://github.com/bergmark/sce

Using servant-0.13.1 and servant-checked-exceptions-2.0.0.0

data BadReq = BadReq deriving Show

deriveJSON defaultOptions ''BadReq

instance ErrStatus BadReq where toErrStatus BadReq = badRequest400

type API = Throws BadReq :> Get '[JSON] Value

api :: Proxy API
api = Proxy

server :: Server API
server = pureErrEnvelope BadReq

main :: IO ()
main = do
  tid <- forkIO (run 8080 $ serve api server)
  manager' <- newManager defaultManagerSettings
  print =<< runClientM (client api) (mkClientEnv manager' (BaseUrl Http "localhost" 8080 ""))
  killThread tid

I expected this to print the Error envelope but instead I get

Left (FailureResponse (Response {responseStatusCode = Status {statusCode = 400, statusMessage = "Bad Request"}, responseHeaders = fromList [("Transfer-Encoding","chunked"),("Date","Sun, 15 Jul 2018 17:53:24 GMT"),("Server","Warp/3.2.22"),("Content-Type","application/json;charset=utf-8")], responseHttpVersion = HTTP/1.1, responseBody = "{\"err\":[]}"}))

If i change the ErrStatus instance to return ok200 I get the envelope as expected:

Right (ErrEnvelope (Identity BadReq))

Is there something I'm missing?

Ability to place Throws/Throwing before multiple grouped route definitions

Currently, this doesn’t work:

data API = Throws MyErr :> "foo" :> Get '[JSON] ()

Specifically, the relevant HasServer instance does not exist, so this is probably related to #4. This would be very useful, since it would allow doing something like this:

data API = Throws MyErr :> ("foo" :> Get '[JSON] () :<|> "bar" :> Get '[JSON] ())

…which helps to reduce boilerplate when a group of APIs all produce the same errors.

add note about having to be careful with letting aeson derive FromJSON and ToJSON instances

By default, Aeson may derive instances for error types that cannot be differentiated from one another.

For example, given the following code:

data FooErr = FooErr deriving (Eq, Read, Show)
 
$(deriveJSON defaultOptions ''FooErr)

data BarErr = BarErr deriving (Eq, Read, Show)
 
$(deriveJSON defaultOptions ''BarErr)

deriveJSON will derive instances that work like the following:

> encode (toErrEnvelope FooErr :: Envelope '[FooErr] Int)
"{\"err\":[]}"
> encode (toErrEnvelope BarErr :: Envelope '[BarErr] Int)
"{\"err\":[]}"

Just by looking at the output JSON, it is not possible to tell whether the error was originally a FooErr or a BarErr.

It is necessary to write the ToJSON and FromJSON instances by hand like the following:

data FooErr = FooErr deriving (Eq, Read, Show)

instance FromJSON FooErr where
  parseJSON = withText "FooErr" $ \case
    "FooErr" -> pure FooErr
    other ->
      fail $ "Trying to parse FooErr, but got \"" <> unpack other <> "\""

instance ToJSON FooErr where { toJSON _ = String "FooErr" }

data BarErr = BarErr deriving (Eq, Read, Show)

instance FromJSON BarErr where
  parseJSON = withText "BarErr" $ \case
    "BarErr" -> pure BarErr
    other ->
      fail $ "Trying to parse BarErr, but got \"" <> unpack other <> "\""

instance ToJSON BarErr where { toJSON _ = String "BarErr" }

This lets the FromJSON and ToJSON instance for Envelope work correctly:

> encode (toErrEnvelope FooErr  :: Envelope '[FooErr ] Int)
"{\"err\":\"FooErr\"}"
> encode (toErrEnvelope BarErr  :: Envelope '[BarErr ] Int)
"{\"err\":\"BarErr\"}"

It would be nice to add a note warning about this somewhere in this package. Probably on the ToJSON and FromJSON instances for Envelope.

add to stackage

It would be nice to add this package to stackage.

Before adding to stackage, I'd like to implement #8. It would also be nice to figure out what to do about #5.

How can we perform case analysis on envelopes?

I'm seeing unusual behaviour with the following piece of client code:

getLocationById   
  :: Integer  
  -> ClientM (Envelope '[InvalidLocationError, NoSuchLocationError] Location) 
getLocationById = client api  
  
main :: IO () 
main = do 
  manager <- newManager defaultManagerSettings
  let env = mkClientEnv manager baseUrl   
  print =<< runClientM program env
  
program :: ClientM () 
program = do  
  liftIO $ putStrLn "Enter the ID of a location (non-negative integer):"  
  locationId <- liftIO readLn 
  result <- getLocationById locationId
  liftIO $ do 
    putStrLn "Result:"
    putStrLn $ catchesEnvelope (show, show) show result

A successful lookup with the program produces the following output:

Enter the ID of a location (non-negative integer):
0
Result:
Location {locationId = 0, locationName = "Amsterdam"}
Right ()

But an unsuccessful lookup produces the following output:

Enter the ID of a location (non-negative integer):
-1
Left (FailureResponse (Response {responseStatusCode = Status {statusCode = 400, statusMessage = "Bad Request"}, responseHeaders = fromList [("Transfer-Encoding","chunked"),("Date","Wed, 13 Feb 2019 01:47:56 GMT"),("Server","Warp/3.2.26"),("Content-Type","application/json;charset=utf-8")], responseHttpVersion = HTTP/1.1, responseBody = "{\"err\":[]}"}))

In the event that calling getLocationById results in an exception (either InvalidLocationError or NoSuchLocationError), I would expect the case analysis of catchesEnvelope to kick in. However, what actually happens is that putStrLn "Result" is never executed.

It seems servant-client returns a Left (FailureResponse ..) in the case of statusCode == 400 or statusCode == 404, causing the above function to short-circuit, meaning that we're unable to perform case analysis.

Is this behaviour intentional? If not, is there a known workaround?

(original source: https://github.com/jonathanknowles/servant-checked-exceptions-example)

Build failure with latest servant libraries

[ 7 of 12] Compiling Servant.Checked.Exceptions.Internal.Servant.Server ( src/Servant/Checked/Exceptions/Internal/Servant/Server
.hs, dist/build/Servant/Checked/Exceptions/Internal/Servant/Server.o )

src/Servant/Checked/Exceptions/Internal/Servant/Server.hs:42:10: warning: [-Wmissing-methods]
    • No explicit implementation for
        ‘hoistServerWithContext’
    • In the instance declaration for
        ‘HasServer (Throws e :> api) context’
   |
42 | instance (HasServer (Throwing '[e] :> api) context) =>
   |          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^...

src/Servant/Checked/Exceptions/Internal/Servant/Server.hs:57:10: warning: [-Wmissing-methods]
    • No explicit implementation for
        ‘hoistServerWithContext’
    • In the instance declaration for
        ‘HasServer (Throwing es :> Verb method status ctypes a) context’
   |
57 | instance (HasServer (Verb method status ctypes (Envelope es a)) context) =>
   |          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^...

src/Servant/Checked/Exceptions/Internal/Servant/Server.hs:72:10: warning: [-Wmissing-methods]
    • No explicit implementation for
        ‘hoistServerWithContext’
    • In the instance declaration for
        ‘HasServer (NoThrow :> Verb method status ctypes a) context’
   |
72 | instance (HasServer (Verb method status ctypes (Envelope '[] a)) context) =>
   |          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^...

Break into combinator/backend packages

We use servant-checked-exceptions in the frontend, and would like to avoid a transitive dependency on servant-server, servant-client, conduit etc.

What do you think of having two packages here servant-checked-exceptions-core (just types and pure functions) and servant-checked-exceptions (instances and functions that depend on server-side code)?

Simplify the From/ToJSON instances for Envelope

Currently, Envelope wraps errors with { "err": ... } and successes with { "data": ... }. These cannot be customized by the user, and they prevent some structures I would like to be able to return. I think the ToJSON instance should simply defer to the underlying ToJSON instances without doing any wrapping to avoid imposing requirements on the user’s responses.

I also question whether or not the FromJSON instance is even a good idea at all. I’m not sure when I would want to construct an Envelope from JSON input, and as the documentation mentions, it’s problematic if the instances are ambiguous. My preference would simply be to eliminate the FromJSON instance entirely.

Build failure with servant 0.16

[3 of 6] Compiling Servant.Checked.Exceptions.Internal.Servant.Server ( src/Servant/Checked/Exceptions/Interna
l/Servant/Server.hs, dist/build/Servant/Checked/Exceptions/Internal/Servant/Server.o )

src/Servant/Checked/Exceptions/Internal/Servant/Server.hs:49:5: error:
    Module
    ‘Servant.Server.Internal.RoutingApplication’
    does not export
    ‘Delayed’
   |
49 |   ( Delayed
   |     ^^^^^^^

src/Servant/Checked/Exceptions/Internal/Servant/Server.hs:50:5: error:
    Module
    ‘Servant.Server.Internal.RoutingApplication’
    does not export
    ‘DelayedIO’
   |
50 |   , DelayedIO
   |     ^^^^^^^^^

src/Servant/Checked/Exceptions/Internal/Servant/Server.hs:51:5: error:
    Module
    ‘Servant.Server.Internal.RoutingApplication’
    does not export
    ‘RouteResult(FailFatal, Route)’
   |
51 |   , RouteResult(FailFatal, Route)
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

src/Servant/Checked/Exceptions/Internal/Servant/Server.hs:52:5: error:
    Module
    ‘Servant.Server.Internal.RoutingApplication’
    does not export
    ‘addAcceptCheck’
   |
52 |   , addAcceptCheck
   |     ^^^^^^^^^^^^^^

And more

throw envelope errors in a short-circuiting monad

It would be nice to be able to throw Envelope errors in some sort of short circuiting monad.

I'm not sure if there is a good / clean / easy way to implement this, but I would be interested in different possibilities.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.