Skip to content

Commit

Permalink
Cookbook
Browse files Browse the repository at this point in the history
  • Loading branch information
theophile-scrive committed Aug 29, 2024
1 parent 26d4023 commit 9835857
Show file tree
Hide file tree
Showing 14 changed files with 282 additions and 66 deletions.
23 changes: 23 additions & 0 deletions .readthedocs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Read the Docs configuration file for Sphinx projects
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details

# Required
version: 2

# Set the OS, Python version and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.12"
# You can also specify other tool versions:
# nodejs: "20"
# rust: "1.70"
# golang: "1.20"

# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: docs/conf.py
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
# builder: "dirhtml"
# Fail on all warnings to avoid broken references
# fail_on_warning: true
53 changes: 26 additions & 27 deletions cabal.project
Original file line number Diff line number Diff line change
@@ -1,56 +1,55 @@
packages:
servant/
servant
servant-client-core
servant-client
servant-docs
servant-foreign
-- servant-http-streams -- 2024-08-29: Commented becase HsOpenSSL uses deprecated APIs
servant-quickcheck
servant-server
servant-swagger

packages:
servant-auth/servant-auth
servant-auth/servant-auth-client
servant-auth/servant-auth-docs
servant-auth/servant-auth-server
servant-auth/servant-auth-swagger

servant-client-core/
servant-client/
servant-docs/
servant-foreign/
servant-http-streams/
servant-quickcheck/
servant-server/
servant-swagger/

doc/tutorial/

-- servant streaming
packages:
servant-machines/
servant-conduit/
servant-pipes/
servant-machines
servant-conduit
servant-pipes

-- servant GHCJS
-- packages:
-- servant-jsaddle/
packages:
doc/tutorial

-- Cookbooks
packages:
doc/cookbook/basic-auth
doc/cookbook/basic-streaming
doc/cookbook/curl-mock
doc/cookbook/custom-errors
doc/cookbook/basic-streaming
doc/cookbook/db-postgres-pool
doc/cookbook/db-sqlite-simple
doc/cookbook/file-upload
doc/cookbook/generic
doc/cookbook/hoist-server-with-context
doc/cookbook/https
doc/cookbook/jwt-and-basic-auth
doc/cookbook/managed-resource
doc/cookbook/multiverb
doc/cookbook/openapi3
doc/cookbook/pagination
-- doc/cookbook/sentry
-- Commented out because servant-quickcheck currently doesn't build.
-- doc/cookbook/testing
doc/cookbook/uverb
doc/cookbook/structuring-apis
doc/cookbook/using-custom-monad
doc/cookbook/using-free-client
doc/cookbook/uverb
-- Commented out because currently doesn't build.
-- doc/cookbook/open-id-connect
doc/cookbook/managed-resource
doc/cookbook/openapi3
-- doc/cookbook/sentry
-- doc/cookbook/testing
-- doc/cookbook/db-mysql-basics


tests: True
optimization: False
Expand Down
13 changes: 8 additions & 5 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
source_suffix = ['.rst', '.md', '.lhs']
source_suffix = {
'.rst': 'restructuredtext',
'.md': 'markdown',
'.lhs': 'markdown',
}


# The master toctree document.
master_doc = 'index'
Expand All @@ -63,7 +68,7 @@
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
language = 'en'

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
Expand Down Expand Up @@ -166,6 +171,4 @@

# -- Markdown -------------------------------------------------------------

source_parsers = {
'.lhs': CommonMarkParser,
}
extensions.append('recommonmark')
4 changes: 2 additions & 2 deletions doc/cookbook/basic-streaming/Streaming.lhs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ main = do
go !acc (S.Yield _ s) = go (acc + 1) s
_ -> do
putStrLn "Try:"
putStrLn "cabal new-run cookbook-basic-streaming server"
putStrLn "cabal new-run cookbook-basic-streaming client 10"
putStrLn "cabal run cookbook-basic-streaming server"
putStrLn "cabal run cookbook-basic-streaming client 10"
putStrLn "time curl -H 'Accept: application/json' localhost:8000/slow/5"
```
22 changes: 0 additions & 22 deletions doc/cookbook/cabal.project

This file was deleted.

Empty file removed doc/cookbook/cabal.project.local
Empty file.
2 changes: 1 addition & 1 deletion doc/cookbook/file-upload/FileUpload.lhs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ main = withSocketsDo . bracket (forkIO startServer) killThread $ \_threadid -> d
If you run this, you should get:
``` bash
$ cabal new-build cookbook-file-upload
$ cabal build cookbook-file-upload
[...]
$ dist-newstyle/build/x86_64-linux/ghc-8.2.1/cookbook-file-upload-0.1/x/cookbook-file-upload/build/cookbook-file-upload/cookbook-file-upload
Inputs:
Expand Down
2 changes: 1 addition & 1 deletion doc/cookbook/generic/Generic.lhs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ main = do
("run-custom-monad":_) -> do
putStrLn "Starting cookbook-generic with a custom monad at http://localhost:8000"
run 8000 (appMyMonad AppCustomState)
_ -> putStrLn "To run, pass 'run' argument: cabal new-run cookbook-generic run"
_ -> putStrLn "To run, pass 'run' argument: cabal run cookbook-generic run"
```
## Using generics together with a custom monad
Expand Down
1 change: 1 addition & 0 deletions doc/cookbook/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ you name it!
using-free-client/UsingFreeClient.lhs
custom-errors/CustomErrors.lhs
uverb/UVerb.lhs
multiverb/MultiVerb.lhs
basic-auth/BasicAuth.lhs
basic-streaming/Streaming.lhs
jwt-and-basic-auth/JWTAndBasicAuth.lhs
Expand Down
178 changes: 178 additions & 0 deletions doc/cookbook/multiverb/MultiVerb.lhs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# MultiVerb: Powerful endpoint types

`MultiVerb` allows you to represent an API endpoint with multiple response types, status codes and headers.

## Preliminaries

```haskell
{-# LANGUAGE GHC2021 #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DerivingVia #-}
import GHC.Generics
import Generics.SOP qualified as GSOP
import Network.Wai.Handler.Warp as Warp
import Servant.API
import Servant.API.MultiVerb
import Servant.Server
import Servant.Server.Generic
```
## Writing an endpoint
Let us create an endpoint that captures an 'Int' and has the following logic:
* If the number is negative, we return status code 400 and an empty body;
* If the number is even, we return a 'Bool' in the response body;
* If the number is odd, we return another 'Int' in the response body.
Let us list all possible HTTP responses:
```haskell
type Responses =
'[ RespondEmpty 400 "Negative"
, Respond 200 "Odd number" Int
, Respond 200 "Even number" Bool
]
```
Let us create the return type:
```haskell
data Result
= NegativeNumber
| Odd Int
| Even Bool
deriving stock (Generic)
deriving (AsUnion Responses)
via GenericAsUnion Responses Result
instance GSOP.Generic Result
```
These deriving statements above tie together the responses and the return values, and the order in which they are defined matters. For instance, if `Even` and `Odd` had switched places in the definition of `Result`, this would provoke an error:
```
• No instance for ‘AsConstructor
((:) @Type Int ('[] @Type)) (Respond 200 "Even number" Bool)’
arising from the 'deriving' clause of a data type declaration
```
> If you would prefer to write an intance of 'AsUnion' by yourself, read more in
the “Implementing AsUnion manually” section.
Finally, let us write our endpoint description:
```haskell
type MultipleChoicesInt =
Capture "int" Int
:> MultiVerb
'GET
'[JSON]
Responses
Result
```
## Integration in a routing table
We want to integrate our endpoint into a wider routing table with another
endpoint: `version`, which returns the version of the API
```haskell
data Routes mode = Routes
{ choicesRoutes :: mode :- "choices" :> Choices
, version :: mode :- "version" :> Get '[JSON] Int
}
deriving stock (Generic)
```
```haskell
type Choices = NamedRoutes Choices'
data Choices' mode = Choices'
{ choices :: mode :- MultipleChoicesInt
}
deriving stock (Generic)
choicesServer :: Choices' AsServer
choicesServer =
Choices'
{ choices = choicesHandler
}
routesServer :: Routes AsServer
routesServer =
Routes
{ choicesRoutes = choicesServer
, version = versionHandler
}
choicesHandler :: Int -> Handler Result
choicesHandler parameter =
if parameter < 0
then pure NegativeNumber
else
if even parameter
then pure $ Odd 3
else pure $ Even True
versionHandler :: Handler Int
versionHandler = pure 1
```
We can now plug everything together:
```haskell
main :: IO ()
main = do
putStrLn "Starting server on http://localhost:5000"
let server = genericServe routesServer
Warp.run 5000 server
```
Now let us run the server and observe how it behaves:
```
$ http http://localhost:5000/version
HTTP/1.1 200 OK
Content-Type: application/json;charset=utf-8
Date: Thu, 29 Aug 2024 14:22:20 GMT
Server: Warp/3.4.1
Transfer-Encoding: chunked
1
```
```
$ http http://localhost:5000/choices/3
HTTP/1.1 200 OK
Content-Type: application/json;charset=utf-8
Date: Thu, 29 Aug 2024 14:22:30 GMT
Server: Warp/3.4.1
Transfer-Encoding: chunked
true
```
```
$ http http://localhost:5000/choices/2
HTTP/1.1 200 OK
Content-Type: application/json;charset=utf-8
Date: Thu, 29 Aug 2024 14:22:33 GMT
Server: Warp/3.4.1
Transfer-Encoding: chunked
3
```
```
$ http http://localhost:5000/choices/-432
HTTP/1.1 400 Bad Request
Date: Thu, 29 Aug 2024 14:22:41 GMT
Server: Warp/3.4.1
Transfer-Encoding: chunked
```
Loading

0 comments on commit 9835857

Please sign in to comment.