Compare commits

..

6 commits

Author SHA1 Message Date
Amar
171195010c failing test case for static client 2016-04-14 17:30:38 +08:00
Sönke Hahn
d37b6a12df update servant-server's changelog for 0.6.1 2016-04-14 17:30:38 +08:00
Sönke Hahn
be4f08a4fb add one more auth test
just to clarify on how to use it properly
2016-04-14 17:30:38 +08:00
Sönke Hahn
353b1aada0 version bump 2016-04-14 17:30:38 +08:00
Sönke Hahn
3875aa82ba sources.txt: prefer servant-server and servant-client
I think both for the release script and for CI it makes sense to prefer these
two packages.
2016-04-14 17:30:38 +08:00
Amar
3375a33b4a WIP - Mappable 2016-04-07 20:54:11 +08:00
440 changed files with 8307 additions and 31351 deletions

12
.github/FUNDING.yml vendored
View file

@ -1,12 +0,0 @@
# These are supported funding model platforms
github: [arianvp]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: https://github.com/haskell-servant/haskell-servant.github.io/blob/hakyll/consultancies.md

View file

@ -1,14 +0,0 @@
#!/usr/bin/env bash
#
# cabal v2-test does not work with GHCJS
# See: https://github.com/haskell/cabal/issues/6175
#
# This invokes cabal-plan to figure out test binaries, and invokes them with node.
cabal-plan list-bins '*:test:*' | while read -r line
do
testpkg=$(echo "$line" | perl -pe 's/:.*//')
testexe=$(echo "$line" | awk '{ print $2 }')
echo "testing $textexe in package $textpkg"
(cd "$testpkg" && node "$testexe".jsexe/all.js)
done

View file

@ -1,148 +0,0 @@
name: CI
# Trigger the workflow on push or pull request, but only for the master branch
on:
pull_request:
push:
branches: [master]
jobs:
cabal:
name: ${{ matrix.os }} / ghc ${{ matrix.ghc }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
cabal: ["3.6"]
ghc:
- "8.6.5"
- "8.8.4"
- "8.10.7"
- "9.0.2"
- "9.2.2"
- "9.4.2"
steps:
- uses: actions/checkout@v2
- uses: haskell/actions/setup@v1
id: setup-haskell-cabal
name: Setup Haskell
with:
ghc-version: ${{ matrix.ghc }}
cabal-version: ${{ matrix.cabal }}
- name: Freeze
run: |
cabal configure --enable-tests --enable-benchmarks --test-show-details=direct
cabal freeze
- uses: actions/cache@v2.1.3
name: Cache ~/.cabal/store and dist-newstyle
with:
path: |
${{ steps.setup-haskell-cabal.outputs.cabal-store }}
dist-newstyle
key: ${{ runner.os }}-${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }}
restore-keys: |
${{ runner.os }}-${{ matrix.ghc }}-
- name: Configure
run: |
cabal install --ignore-project -j2 doctest --constraint='doctest ^>=0.20'
- name: Build
run: |
cabal build all
- name: Test
run: |
cabal test all
- name: Run doctests
run: |
# Necessary for doctest to be found in $PATH
export PATH="$HOME/.cabal/bin:$PATH"
DOCTEST="cabal repl --with-ghc=doctest --ghc-options=-w"
(cd servant && eval $DOCTEST)
(cd servant-client && eval $DOCTEST)
(cd servant-client-core && eval $DOCTEST)
(cd servant-http-streams && eval $DOCTEST)
(cd servant-docs && eval $DOCTEST)
(cd servant-foreign && eval $DOCTEST)
(cd servant-server && eval $DOCTEST)
(cd servant-machines && eval $DOCTEST)
(cd servant-conduit && eval $DOCTEST)
(cd servant-pipes && eval $DOCTEST)
# stack:
# name: stack / ghc ${{ matrix.ghc }}
# runs-on: ubuntu-latest
# strategy:
# matrix:
# stack: ["2.7.5"]
# ghc: ["8.10.7"]
# steps:
# - uses: actions/checkout@v2
# - uses: haskell/actions/setup@v1
# name: Setup Haskell Stack
# with:
# ghc-version: ${{ matrix.ghc }}
# stack-version: ${{ matrix.stack }}
# - uses: actions/cache@v2.1.3
# name: Cache ~/.stack
# with:
# path: ~/.stack
# key: ${{ runner.os }}-${{ matrix.ghc }}-stack
# - name: Install dependencies
# run: |
# stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks --only-dependencies
# - name: Build
# run: |
# stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks
# - name: Test
# run: |
# stack test --system-ghc
ghcjs:
name: ubuntu-latest / ghcjs 8.6
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v2
- uses: cachix/install-nix-action@v13
with:
extra_nix_config: |
trusted-public-keys = ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=1aba6f367982bd6dd78ec2fda75ab246a62d32c5 cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
substituters = https://nixcache.reflex-frp.org https://cache.nixos.org/
- name: Setup
run: |
# Override cabal.project with the lightweight GHCJS one
cp cabal.ghcjs.project cabal.project
cat cabal.project
nix-shell ghcjs.nix --run "cabal v2-update && cabal v2-freeze"
- uses: actions/cache@v2.1.3
name: Cache ~/.cabal/store and dist-newstyle
with:
path: |
~/.cabal/store
dist-newstyle
key: ${{ runner.os }}-ghcjs8.6-${{ hashFiles('cabal.project.freeze') }}
restore-keys: |
${{ runner.os }}-ghcjs8.6-
- name: Build
run: |
nix-shell ghcjs.nix --run "cabal v2-build --ghcjs --enable-tests --enable-benchmarks all"
- name: Tests
run: |
nix-shell ghcjs.nix --run ".github/run-ghcjs-tests.sh"

13
.gitignore vendored
View file

@ -1,7 +1,4 @@
**/*/dist
*~
dist-*
.ghc.environment.*
/bin
/lib
/share
@ -26,15 +23,9 @@ cabal.config
*.hp
Setup
.stack-work
shell.nix
default.nix
doc/_build
doc/venv
doc/tutorial/static/api.js
doc/tutorial/static/jq.js
shell.nix
.hspec-failures
# nix
result*
# local versions of things
servant-multipart

View file

@ -15,14 +15,6 @@ steps:
# # true.
# add_language_pragma: true
# Align the right hand side of some elements. This is quite conservative
# and only applies to statements where each element occupies a single
# line.
- simple_align:
cases: false
top_level_patterns: false
records: false
# Import cleanup
- imports:
# There are different ways we can align names and lists.
@ -41,91 +33,6 @@ steps:
# Default: global.
align: global
# Folowing options affect only import list alignment.
#
# List align has following options:
#
# - after_alias: Import list is aligned with end of import including
# 'as' and 'hiding' keywords.
#
# > import qualified Data.List as List (concat, foldl, foldr, head,
# > init, last, length)
#
# - with_alias: Import list is aligned with start of alias or hiding.
#
# > import qualified Data.List as List (concat, foldl, foldr, head,
# > init, last, length)
#
# - new_line: Import list starts always on new line.
#
# > import qualified Data.List as List
# > (concat, foldl, foldr, head, init, last, length)
#
# Default: after_alias
list_align: new_line
# Long list align style takes effect when import is too long. This is
# determined by 'columns' setting.
#
# - inline: This option will put as much specs on same line as possible.
#
# - new_line: Import list will start on new line.
#
# - new_line_multiline: Import list will start on new line when it's
# short enough to fit to single line. Otherwise it'll be multiline.
#
# - multiline: One line per import list entry.
# Type with contructor list acts like single import.
#
# > import qualified Data.Map as M
# > ( empty
# > , singleton
# > , ...
# > , delete
# > )
#
# Default: inline
long_list_align: new_line
# Align empty list (importing instances)
#
# Empty list align has following options
#
# - inherit: inherit list_align setting
#
# - right_after: () is right after the module name:
#
# > import Vector.Instances ()
#
# Default: inherit
empty_list_align: right_after
# List padding determines indentation of import list on lines after import.
# This option affects 'long_list_align'.
#
# - <integer>: constant value
#
# - module_name: align under start of module name.
# Useful for 'file' and 'group' align settings.
list_padding: module_name
# Separate lists option affects formating of import list for type
# or class. The only difference is single space between type and list
# of constructors, selectors and class functions.
#
# - true: There is single space between Foldable type and list of it's
# functions.
#
# > import Data.Foldable (Foldable (fold, foldl, foldMap))
#
# - false: There is no space between Foldable type and list of it's
# functions.
#
# > import Data.Foldable (Foldable(fold, foldl, foldMap))
#
# Default: true
separate_lists: true
# Language pragmas
- language_pragmas:
# We can generate different styles of language pragma lists.
@ -140,20 +47,13 @@ steps:
# Default: vertical.
style: vertical
# Align affects alignment of closing pragma brackets.
#
# - true: Brackets are aligned in same collumn.
#
# - false: Brackets are not aligned together. There is only one space
# between actual import and closing bracket.
#
# Default: true
align: true
# stylish-haskell can detect redundancy of some language pragmas. If this
# is set to true, it will remove those redundant pragmas. Default: true.
remove_redundant: true
# Align the types in record declarations
- records: {}
# Replace tabs by spaces. This is disabled by default.
# - tabs:
# # Number of spaces to use for each tab. Default: 8, as specified by the
@ -167,24 +67,11 @@ steps:
# to. Different steps take this into account. Default: 80.
columns: 80
# By default, line endings are converted according to the OS. You can override
# preferred format here.
#
# - native: Native newline format. CRLF on Windows, LF on other OSes.
#
# - lf: Convert to LF ("\n").
#
# - crlf: Convert to CRLF ("\r\n").
#
# Default: native.
newline: lf
# Sometimes, language extensions are specified in a cabal file or from the
# command line instead of using language pragmas in the file. stylish-haskell
# needs to be aware of these, so it can parse the file correctly.
#
# No language extensions are enabled by default.
language_extensions:
- FlexibleContexts
- TemplateHaskell
- QuasiQuotes
- TemplateHaskell
- QuasiQuotes

32
.travis.yml Normal file
View file

@ -0,0 +1,32 @@
sudo: false
language: c
env:
- GHCVER=7.8.4
- GHCVER=7.10.2
addons:
apt:
sources:
- hvr-ghc
packages:
- ghc-7.8.4
- ghc-7.10.2
- cabal-install-1.22
- libgmp-dev
install:
- (mkdir -p $HOME/.local/bin && cd $HOME/.local/bin && wget https://zalora-public.s3.amazonaws.com/tinc && chmod +x tinc)
- export PATH=/opt/ghc/$GHCVER/bin:/opt/cabal/1.22/bin:$PATH
- ghc --version
- cabal --version
- travis_retry cabal update
- sed -i 's/^jobs:/-- jobs:/' ${HOME}/.cabal/config
script:
- ./travis.sh
cache:
directories:
- $HOME/.tinc/cache

View file

@ -8,12 +8,11 @@ repository. You can use `cabal`:
./scripts/test-all.sh # Run all the tests
```
Or `stack`:
`stack`:
```shell
stack setup # Downloads and installs a proper GHC version if necessary
stack build --fast --pedantic # Install dependencies and build packages
stack test # Run all the tests
stack build # Install and build packages
stack test # Run all the tests
```
Or `nix`:
@ -21,7 +20,6 @@ Or `nix`:
./scripts/generate-nix-files.sh # Get up-to-date shell.nix files
```
To build the docs, see `doc/README.md`.
## General
@ -30,39 +28,13 @@ Some things we like:
- Explicit imports
- Upper and lower bounds for packages
- Few dependencies
- -Werror-compatible (7.8, 7.10 and 8.0)
- -Werror-compatible (for both 7.8 and 7.10)
Though we aren't sticklers for style, the `.stylish-haskell.yaml` and `HLint.hs`
files in the repository provide a good baseline for consistency.
**Important**: please do not modify the versions of the servant packages you are sending patches for.
## Changelog entries
We experiment with using [changelog-d tool](https://github.com/phadej/changelog-d) to assemble changelogs.
You are not required to install it.
In each PR please add a file to `changelog.d` directory named after issue you are solving or the pull request itself (in a separate commit after you know the pull request number). For example
```cabal
synopsis: One sentence summary of the change.
prs: #1219
issues: #1028
description: {
A longer description. Small changes don't need this.
Bigger ones definitely do, for example we try to include migration hints
for breaking changes.
However if you don't know what to write, that's ok too.
By the way, the braces around are omitted when the file is parsed.
They can be used so the field doesn't need to be indented, which is handy
for prose.
}
```
Please include a description of the changes in your PR in the `CHANGELOG.md` of
the packages you've changed. And of course, write tests!
## PR process
@ -79,18 +51,15 @@ not been a timely response to a PR, you can ping the Maintainers group (with
We encourage people to experiment with new combinators and instances - it is
one of the most powerful ways of using `servant`, and a wonderful way of
getting to know it better. If you do write a new combinator, we would love to
know about it! Either hop on
[#haskell-servant on libera.chat](https://web.libera.chat/#haskell-servant) and
let us know, or open an issue with the `news` tag (which we will close when we
read it).
know about it! Either hop on #servant on freenode and let us know, or open an
issue with the `news` tag (which we will close when we read it).
As for adding them to the main repo: maintaining combinators can be expensive,
since official combinators must have instances for all classes (and new classes
come along fairly frequently). We therefore have to be quite selective about
those that we accept. If you're considering writing a new combinator, open an
issue to discuss it first! Or contribute it to the
[servant-contrib](https://github.com/haskell-servant/servant-contrib) repository.
You could release your combinator as a separate package, of course.
issue to discuss it first! (You could release your combinator as a separate
package, of course.)
## New classes
@ -105,7 +74,7 @@ the `news` label if you make a new package so we can know about it!
## Release policy
We are currently moving to a more aggressive release policy, so that you can get
We are currently moving to a more aggresive release policy, so that you can get
what you contribute from Hackage fairly soon. However, note that prior to major
releases it may take some time in between releases.

6
HLint.hs Normal file
View file

@ -0,0 +1,6 @@
import "hint" HLint.Default
ignore "Redundant do"
ignore "Parse error"
ignore "Use list comprehension"
ignore "Use liftM"

View file

@ -4,68 +4,17 @@
## Getting Started
We have a [tutorial](http://docs.servant.dev/en/stable/tutorial/index.html) that
We have a [tutorial](http://haskell-servant.github.io/tutorial) that
introduces the core features of servant. After this article, you should be able
to write your first servant webservices, learning the rest from the haddocks'
examples.
The core documentation can be found [here](http://docs.servant.dev/).
Other blog posts, videos and slides can be found on the
[website](http://www.servant.dev/).
[website](http://haskell-servant.github.io/).
If you need help, drop by the IRC channel (#haskell-servant on libera.chat) or [mailing
If you need help, drop by the IRC channel (#servant on freenode) or [mailing
list](https://groups.google.com/forum/#!forum/haskell-servant).
## Contributing
See `CONTRIBUTING.md`
## Release process outline (by phadej)
- Update changelog and bump versions in `master`
- `git log --oneline v0.12.. | grep 'Merge pull request'` is a good starting point (use correct previous release tag)
- Create a release branch, e.g. `release-0.13`
- Release branch is useful for backporting fixes from `master`
- Smoke test in [`servant-universe`](https://github.com/phadej/servant-universe)
- `git submodule foreach git checkout master` and `git submodule foreach git pull` to get newest of everything.
- `cabal new-build --enable-tests all` to verify that everything builds, and `cabal new-test all` to run tests
- It's a good idea to separate these steps, as tests often pass, if they compile :)
- See `cabal.project` to selectively `allow-newer`
- If some packages are broken, on your discretisation there are two options:
- Fix them and make PRs: it's a good idea to test against older `servant` version too.
- Temporarily comment out broken package
- If you make a commit for `servant-universe`, you can use it as submodule in private projects to test even more
- When ripples are cleared out:
- `git tag -s` the release
- `git push --tags`
- `cabal sdist` and `cabal upload`
## TechEmpower framework benchmarks
We develop and maintain the servant TFB entry in https://github.com/haskell-servant/FrameworkBenchmarks/
To verify (i.e. compile and test that it works)
```sh
./tfb --mode verify --test servant servant-beam servant-psql-simple --type json plaintext db fortune
```
To compare with `warp`
```sh
./tfb --mode benchmark --test warp servant servant-beam servant-psql-simple --type json plaintext db fortune
```
To compare with `reitit` (Clojure framework)
```sh
./tfb --mode benchmark --test reitit reitit-async reitit-jdbc servant servant-beam servant-psql-simple --type json plaintext db fortune
```
You can see the visualised results at https://www.techempower.com/benchmarks/#section=test
## Nix
A developer shell.nix file is provided in the `nix` directory
See [nix/README.md](nix/README.md)

View file

@ -1,14 +0,0 @@
-- Using https://launchpad.net/~hvr/+archive/ubuntu/ghcjs
packages:
servant/
servant-client/
servant-client-core/
-- we need to tell cabal we are using GHCJS
compiler: ghcjs
tests: True
-- Constraints so that reflex-platform provided packages are selected.
constraints: attoparsec == 0.13.2.2
constraints: hashable == 1.3.0.0

View file

@ -1,54 +0,0 @@
packages:
servant/
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/
servant-client-core/
servant-http-streams/
servant-docs/
servant-foreign/
servant-server/
servant-swagger/
doc/tutorial/
-- servant streaming
packages:
servant-machines/
servant-conduit/
servant-pipes/
-- servant GHCJS
-- packages:
-- servant-jsaddle/
-- Cookbooks
packages:
doc/cookbook/basic-auth
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/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/open-id-connect
doc/cookbook/managed-resource
tests: True
optimization: False
-- reorder-goals: True

View file

@ -1,9 +0,0 @@
synopsis: Fixes encoding of URL parameters in servant-client
prs: #1432
issues: #1418
description: {
Some applications use query parameters to pass arbitrary (non-unicode) binary
data. This change modifies how servant-client handles query parameters, so
that application developers can use `ToHttpApiData` to marshal binary data into
query parameters.
}

View file

@ -1,11 +0,0 @@
synopsis: Derive HasClient good response status from Verb status
prs: #1469
description: {
`HasClient` instances for the `Verb` datatype use `runRequest` in
`clientWithRoute` definitions.
This means that a request performed with `runClientM` will be successful if and
only if the endpoint specify a response status code >=200 and <300.
This change replaces `runRequest` with `runRequestAcceptStatus` in `Verb`
instances for the `HasClient` class, deriving the good response status from
the `Verb` status.
}

View file

@ -1,9 +0,0 @@
synopsis: Enable FlexibleContexts in Servant.API.ContentTypes
prs: #1477
description: {
Starting with GHC 9.2, UndecidableInstances no longer implies FlexibleContexts.
Add this extension where it's needed to make compilation succeed.
}

View file

@ -1,10 +0,0 @@
synopsis: Fix performRequest in servant-client-ghcjs
prs: #1529
description: {
performRequest function in servant-client-ghcjs was not compatible with the
latest RunClient typeclass. Added the acceptStatus parameter and fixed the
functionality to match what servant-client provides.
}

View file

@ -1,81 +0,0 @@
synopsis: Display capture hints in router layout
prs: #1556
description: {
This PR enhances the `Servant.Server.layout` function, which produces a textual description of the routing layout of an API. More precisely, it changes `<capture>` blocks, so that they display the name and type of the variable being captured instead.
Example:
For the following API
```haskell
type API =
"a" :> "d" :> Get '[JSON] NoContent
:<|> "b" :> Capture "x" Int :> Get '[JSON] Bool
:<|> "a" :> "e" :> Get '[JSON] Int
```
we previously got the following output:
```
/
├─ a/
│ ├─ d/
│ │ └─•
│ └─ e/
│ └─•
└─ b/
└─ <capture>/
├─•
└─•
```
now we get:
```
/
├─ a/
│ ├─ d/
│ │ └─•
│ └─ e/
│ └─•
└─ b/
└─ <x::Int>/
├─•
└─•
```
This change is achieved by the introduction of a CaptureHint type, which is passed as an extra argument to the CaptureRouter and CaptureAllRouter constructors for the Router' type.
CaptureHint values are then used in routerLayout, to display the name and type of captured values, instead of just `<capture>` previously.
N.B.:
Because the choice smart constructor for routers can aggregate Capture combinators with different capture hints, the Capture*Router constructors actually take a list of CaptureHint, instead of a single one.
This PR also introduces Spec tests for the routerLayout function.
Warning:
This change is potentially breaking, because it adds the constraint `Typeable a` to all types that are to be captured. Because all types are typeable since GHC 7.10, this is not as bad as it sounds ; it only break expressions where `a` is quantified in an expression with `Capture a`.
In those cases, the fix is easy: it suffices to add `Typeable a` to the left-hand side of the quantification constraint.
For instance, the following code will no longer compile:
```haskell
type MyAPI a = Capture "foo" a :> Get '[JSON] ()
myServer :: forall a. Server (MyAPI a)
myServer = const $ return ()
myApi :: forall a. Proxy (MyAPI a)
myApi = Proxy
app :: forall a. (FromHttpApiData a) => Application
app = serve (myApi @a) (myServer @a)
```
Indeed, `app` should be replaced with:
```haskell
app :: forall a. (FromHttpApiData a, Typeable a) => Application
app = serve (myApi @a) (myServer @a)
```
}

View file

@ -1,13 +0,0 @@
synopsis: Encode captures using toEncodedUrlPiece
prs: #1569
issues: #1511
description: {
The `servant-client` library now makes direct use of `toEncodedUrlPiece` from `ToHttpApiData`
to encode captured values when building the request path. It gives user freedom to implement
URL-encoding however they need.
Previous behavior was to use `toUrlPiece` and URL-encode its output using `toEncodedUrlPiece`
from the `Text` instance of `ToHttpApiData`. The issue with this approach is that
`ToHttpApiData Text` is overly zealous and also encodes characters, such as `*`, which are perfectly valid in a URL.
}

View file

@ -1,2 +0,0 @@
synopsis: Add API docs for ServerT
prs: #1573

View file

@ -1,12 +0,0 @@
synopsis: Allow IO in validationKeys
prs: #1580
issues: #1579
description: {
Currently validationKeys are a fixed JWKSet. This does not work with OIDC
providers such as AWS Cognito or Okta, which regularly fetching jwks_uri to
discover new and expired keys.
This change alters the type of validationKeys from JWKSet to IO JWKSet.
}

View file

@ -1,2 +0,0 @@
synopsis: Only include question mark for nonempty query strings
prs: 1589

View file

@ -1,2 +0,0 @@
synopsis: Run ClientEnv's makeClientRequest in IO.
prs: #1595

View file

@ -1,10 +0,0 @@
synopsis: Handle Cookies correctly for RunStreamingClient
prs: #1606
issues: #1605
description: {
Makes performWithStreamingRequest take into consideration the
CookieJar, which it previously didn't.
}

View file

@ -1,2 +0,0 @@
synopsis: Add Functor instance to AuthHandler.
prs: #1638

View file

@ -1,8 +0,0 @@
synopsis: Add HasStatus instance for Headers (that defers StatusOf to underlying value)
prs: #1649
description: {
Adds a new HasStatus (Headers hs a) instance (StatusOf (Headers hs a) = StatusOf a)
}

View file

@ -1,2 +0,0 @@
organization: haskell-servant
repository: servant

View file

@ -1,16 +0,0 @@
synopsis: Add sample cURL requests to generated documentation
prs: #1401
description: {
Add sample cURL requests to generated documentation.
Those supplying changes to the Request `header` field manually using
lenses will need to add a sample bytestring value.
`headers <>~ ["unicorn"]`
becomes
`headers <>~ [("unicorn", "sample value")]`
}

View file

@ -1,38 +0,0 @@
with (builtins.fromJSON (builtins.readFile ./nix/nixpkgs.json));
{
pkgs ? import (builtins.fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/${rev}.tar.gz";
inherit sha256;
}) {}
, compiler ? "ghc883"
}:
let
overrides = self: super: {
servant = self.callCabal2nix "servant" ./servant {};
servant-docs = self.callCabal2nix "servant-docs" ./servant-docs {};
servant-pipes = self.callCabal2nix "servant-pipes" ./servant-pipes {};
servant-server = self.callCabal2nix "servant-server" ./servant-server {};
servant-client = self.callCabal2nix "servant-client" ./servant-client {};
servant-foreign = self.callCabal2nix "servant-foreign" ./servant-foreign {};
servant-conduit = self.callCabal2nix "servant-conduit" ./servant-conduit {};
servant-machines = self.callCabal2nix "servant-machines" ./servant-machines {};
servant-client-core = self.callCabal2nix "servant-client-core" ./servant-client-core {};
servant-http-streams = self.callCabal2nix "servant-http-streams" ./servant-http-streams {};
};
hPkgs = pkgs.haskell.packages.${compiler}.override { inherit overrides; };
in
with hPkgs;
{
inherit
servant
servant-client
servant-client-core
servant-conduit
servant-docs
servant-foreign
servant-http-streams
servant-machines
servant-pipes
servant-server;
}

View file

@ -1,22 +1,216 @@
# Minimal makefile for Sphinx documentation
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SPHINXPROJ = Servant
SOURCEDIR = .
PAPER =
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help
help:
@if [ ! -d venv ]; then echo "WARNING: There is no venv directory, did you forget to 'virtualenv venv'. Check README.md."; fi
@if [ ! "z$$(which $(SPHINXBUILD))" = "z$$(pwd)/venv/bin/sphinx-build" ]; then echo "WARNING: Did you forgot to 'source venv/bin/activate'"; fi
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " applehelp to make an Apple Help Book"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@echo " coverage to run coverage check of the documentation (if enabled)"
.PHONY: help Makefile
.PHONY: clean
clean:
rm -rf $(BUILDDIR)/*
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: html
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
.PHONY: dirhtml
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
.PHONY: singlehtml
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
.PHONY: pickle
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
.PHONY: json
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
.PHONY: htmlhelp
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
.PHONY: qthelp
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/generics-eot.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/generics-eot.qhc"
.PHONY: applehelp
applehelp:
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
@echo
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
@echo "N.B. You won't be able to view it unless you put it in" \
"~/Library/Documentation/Help or install it in your application" \
"bundle."
.PHONY: devhelp
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/generics-eot"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/generics-eot"
@echo "# devhelp"
.PHONY: epub
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
.PHONY: latex
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
.PHONY: latexpdf
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: latexpdfja
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: text
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
.PHONY: man
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
.PHONY: texinfo
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
.PHONY: info
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
.PHONY: gettext
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
.PHONY: changes
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
.PHONY: linkcheck
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
.PHONY: doctest
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
.PHONY: coverage
coverage:
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
@echo "Testing of coverage in the sources finished, look at the " \
"results in $(BUILDDIR)/coverage/python.txt."
.PHONY: xml
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
.PHONY: pseudoxml
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."

View file

@ -1,8 +0,0 @@
To build the docs locally:
$ virtualenv venv
$ . ./venv/bin/activate
$ pip install -r requirements.txt
$ make html
Docs will be built in _build/html/index.html .

8
doc/building-the-docs Normal file
View file

@ -0,0 +1,8 @@
To build the docs locally:
$ virtualenv venv
$ . ./venv/bin/activate
$ pip install -r requirements.txt
$ make html
Docs will be built in _build/html/index.html .

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Servant documentation build configuration file, created by
# sphinx-quickstart on Fri Jul 6 11:38:51 2018.
# servant documentation build configuration file, created by
# sphinx-quickstart on Mon Nov 23 13:24:36 2015.
#
# This file is execfile()d with the current directory set to its
# containing dir.
@ -12,21 +12,20 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys
import os
import shlex
from recommonmark.parser import CommonMarkParser
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
from recommonmark.parser import CommonMarkParser
#sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
@ -38,15 +37,17 @@ templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
source_suffix = ['.rst', '.md', '.lhs']
source_suffix = ['.md', '.rst', '.lhs']
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'Servant'
copyright = u'2022, Servant Contributors'
project = u'servant'
copyright = u'2016, Servant Contributors'
author = u'Servant Contributors'
# The version info for the project you're documenting, acts as replacement for
@ -54,9 +55,9 @@ author = u'Servant Contributors'
# built documents.
#
# The short X.Y version.
# version = u'0.14'
# version = 'latest'
# The full version, including alpha/beta/rc tags.
# release = u'0.14'
# release = 'latest'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@ -65,14 +66,45 @@ author = u'Servant Contributors'
# Usually you set "language" from the command line for these cases.
language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
exclude_patterns = ['_build', 'venv']
# The reST default role (used for this markup: `text`) to use for all
# documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
def setup(app):
from sphinx.highlighting import lexers
from pygments.lexers import HaskellLexer
lexers['haskell ignore'] = HaskellLexer(stripnl=False)
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
@ -81,77 +113,157 @@ todo_include_todos = False
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# This is required for the alabaster theme
# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
html_sidebars = {
'**': [
'relations.html', # needs 'show_related': True theme option to display
'searchbox.html',
]
}
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#html_extra_path = []
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# -- Options for HTMLHelp output ------------------------------------------
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Language to be used for generating the HTML full-text search index.
# Sphinx supports the following languages:
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
#html_search_language = 'en'
# A dictionary with options for the search language support, empty by default.
# Now only 'ja' uses this config value
#html_search_options = {'type': 'default'}
# The name of a javascript file (relative to the configuration directory) that
# implements a search results scorer. If empty, the default will be used.
#html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
htmlhelp_basename = 'Servantdoc'
htmlhelp_basename = 'servantdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
# Latex figure (float) alignment
#'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'Servant.tex', u'Servant Documentation',
(master_doc, 'servant.tex', u'servant Documentation',
u'Servant Contributors', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'servant', u'Servant Documentation',
(master_doc, 'servant', u'servant Documentation',
[author], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
@ -159,13 +271,24 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'Servant', u'Servant Documentation',
author, 'Servant', 'One line description of project.',
'Miscellaneous'),
(master_doc, 'servant', u'servant Documentation',
author, 'servant', 'One line description of project.',
'Miscellaneous'),
]
# -- Markdown -------------------------------------------------------------
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False
source_parsers = {
'.md': CommonMarkParser,
'.lhs': CommonMarkParser,
}

View file

@ -1,179 +0,0 @@
# Basic Authentication
Let's see a simple example of a web application with a
single endpoint, protected by
[Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication).
First, some throat clearing.
``` haskell
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeOperators #-}
import Control.Concurrent
import Control.Exception
import qualified Data.Map as Map
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8)
import Network.HTTP.Client (newManager, defaultManagerSettings)
import Network.Wai.Handler.Warp
import Servant
import Servant.Client
```
We will be dealing with a very simple model of users, as shown below.
Our "user database" will just be a map from usernames to full user details.
For the sake of simplicity, it will just be read only but the same code could
be used with mutable references, database connections, files and more in place
of our `Map`.
``` haskell
type Username = T.Text
type Password = T.Text
type Website = T.Text
data User = User
{ user :: Username
, pass :: Password
, site :: Website
} deriving (Eq, Show)
-- could be a postgres connection, a file, anything.
type UserDB = Map.Map Username User
-- create a "database" from a list of users
createUserDB :: [User] -> UserDB
createUserDB users = Map.fromList [ (user u, u) | u <- users ]
-- our test database
userDB :: UserDB
userDB = createUserDB
[ User "john" "shhhh" "john.com"
, User "foo" "bar" "foobar.net"
]
```
Our API will contain a single endpoint, returning the authenticated
user's own website.
``` haskell
-- a 'GET /mysite' endpoint, protected by basic authentication
type API = BasicAuth "People's websites" User :> "mysite" :> Get '[JSON] Website
{- if there were more endpoints to be protected, one could write:
type API = BasicAuth "People's websites" User :>
( "foo" :> Get '[JSON] Foo
:<|> "bar" :> Get '[JSON] Bar
)
-}
api :: Proxy API
api = Proxy
server :: Server API
server usr = return (site usr)
```
In order to protect our endpoint (`"mysite" :> Get '[JSON] Website`), we simply
drop the `BasicAuth` combinator in front of it. Its first parameter,
`"People's websites"` in our example, is the realm, which is an arbitrary string
identifying the protected resources. The second parameter, `User` in our example,
corresponds to the type we want to use to represent authenticated users. It could
be anything.
When using `BasicAuth` in an API, the server implementation "gets" an argument
of the authenticated user type used with `BasicAuth`, `User` in our case, in the
"corresponding spot". In this example, the server implementation simply returns
the `site` field of the authenticated user. More realistic applications would
have endpoints that take other arguments and where a lot more logic would
be implemented. But in a sense, `BasicAuth` adds an argument just like `Capture`,
`QueryParam`, `ReqBody` and friends. But instead of performing some form of
decoding logic behind the scenes, servant runs some "basic auth check" that the
user provides.
In our case, we need access to our user database, so we simply
take it as an argument. A more serious implementation would probably take
a database connection or even a connection pool.
``` haskell
-- provided we are given a user database, we can supply
-- a function that checks the basic auth credentials
-- against our database.
checkBasicAuth :: UserDB -> BasicAuthCheck User
checkBasicAuth db = BasicAuthCheck $ \basicAuthData ->
let username = decodeUtf8 (basicAuthUsername basicAuthData)
password = decodeUtf8 (basicAuthPassword basicAuthData)
in
case Map.lookup username db of
Nothing -> return NoSuchUser
Just u -> if pass u == password
then return (Authorized u)
else return BadPassword
```
This check simply looks up the user in the "database" and makes sure the
right password was used. For reference, here are the definitions of
`BasicAuthResult` and `BasicAuthCheck`:
```
-- | The result of authentication/authorization
data BasicAuthResult usr
= Unauthorized
| BadPassword
| NoSuchUser
| Authorized usr
deriving (Eq, Show, Read, Generic, Typeable, Functor)
-- | Datatype wrapping a function used to check authentication.
newtype BasicAuthCheck usr = BasicAuthCheck
{ unBasicAuthCheck :: BasicAuthData
-> IO (BasicAuthResult usr)
}
deriving (Generic, Typeable, Functor)
```
This is all great, but how is our `BasicAuth` combinator supposed to know
that it should use our `checkBasicAuth` from above? The answer is that it
simply expects to find a `BasicAuthCheck` value for the right user type in
the `Context` with which we serve the application, where `Context` is just
servant's way to allow users to communicate some configuration of sorts to
combinators. It is nothing more than an heterogeneous list and we can create
a context with our auth check and run our application with it with the following
code:
``` haskell
runApp :: UserDB -> IO ()
runApp db = run 8080 (serveWithContext api ctx server)
where ctx = checkBasicAuth db :. EmptyContext
```
`ctx` above is just a context with one element, `checkBasicAuth db`,
whose type is `BasicAuthCheck User`. In order to say that we want to serve our
application using the supplied context, we just have to use `serveWithContext`
in place of `serve`.
Finally, let's derive a client to this endpoint as well in order to see our
server in action!
``` haskell
getSite :: BasicAuthData -> ClientM Website
getSite = client api
main :: IO ()
main = do
mgr <- newManager defaultManagerSettings
bracket (forkIO $ runApp userDB) killThread $ \_ ->
runClientM (getSite u) (mkClientEnv mgr (BaseUrl Http "localhost" 8080 ""))
>>= print
where u = BasicAuthData "foo" "bar"
```
This program prints `Right "foobar.net"`, as expected. Feel free to change this
code and see what happens when you specify credentials that are not in the
database.
The entire program covered here is available as a literate Haskell file
[here](https://github.com/haskell-servant/servant/tree/master/doc/cookbook/basic-auth),
along with a `cabal` project.

View file

@ -1,29 +0,0 @@
cabal-version: 2.2
name: cookbook-basic-auth
version: 0.1
synopsis: Basic Authentication cookbook example
homepage: http://docs.servant.dev/
license: BSD-3-Clause
license-file: ../../../servant/LICENSE
author: Servant Contributors
maintainer: haskell-servant-maintainers@googlegroups.com
build-type: Simple
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
executable cookbook-basic-auth
main-is: BasicAuth.lhs
build-depends: base == 4.*
, text >= 1.2
, aeson >= 1.2
, containers >= 0.5
, servant
, servant-client
, servant-server
, warp >= 3.2
, wai >= 3.2
, http-types >= 0.12
, markdown-unlit >= 0.4
, http-client >= 0.5
default-language: Haskell2010
ghc-options: -Wall -pgmL markdown-unlit
build-tool-depends: markdown-unlit:markdown-unlit

View file

@ -1,129 +0,0 @@
# Streaming out-of-the-box
In other words, without streaming libraries.
## Introduction
- Servant supports streaming
- Some basic usage doesn't require usage of streaming libraries,
like `conduit`, `pipes`, `machines` or `streaming`.
We have bindings for them though.
- Similar example is bundled with each of our streaming library interop packages (see
[servant-pipes](https://github.com/haskell-servant/servant/blob/master/servant-pipes/example/Main.hs),
[servant-conduit](https://github.com/haskell-servant/servant/blob/master/servant-conduit/example/Main.hs) and
[servant-machines](https://github.com/haskell-servant/servant/blob/master/servant-machines/example/Main.hs))
- `SourceT` doesn't have *Prelude* with handy combinators, so we have to write
things ourselves. (Note to self: `mapM` and `foldM` would be handy to have).
## Code
```haskell
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TypeOperators #-}
module Main (main) where
import Control.Concurrent
(threadDelay)
import Control.Monad.IO.Class
(MonadIO (..))
import qualified Data.ByteString as BS
import Data.Maybe
(fromMaybe)
import Network.HTTP.Client
(defaultManagerSettings, newManager)
import Network.Wai
(Application)
import System.Environment
(getArgs, lookupEnv)
import Text.Read
(readMaybe)
import Servant
import Servant.Client.Streaming
import qualified Servant.Types.SourceT as S
import qualified Network.Wai.Handler.Warp as Warp
type FastAPI = "get" :> Capture "num" Int :> StreamGet NewlineFraming JSON (SourceIO Int)
type API = FastAPI
:<|> "slow" :> Capture "num" Int :> StreamGet NewlineFraming JSON (SourceIO Int)
-- monad can be ResourceT IO too.
:<|> "readme" :> StreamGet NoFraming OctetStream (SourceIO BS.ByteString)
-- we can have streaming request body
:<|> "proxy"
:> StreamBody NoFraming OctetStream (SourceIO BS.ByteString)
:> StreamPost NoFraming OctetStream (SourceIO BS.ByteString)
api :: Proxy API
api = Proxy
server :: Server API
server = fast :<|> slow :<|> readme :<|> proxy where
fast n = liftIO $ do
putStrLn $ "/get/" ++ show n
return $ fastSource n
slow n = liftIO $ do
putStrLn $ "/slow/" ++ show n
return $ slowSource n
readme = liftIO $ do
putStrLn "/proxy"
return (S.readFile "README.md")
proxy c = liftIO $ do
putStrLn "/proxy"
return c
-- for some reason unfold leaks?
fastSource = S.fromStepT . mk where
mk m
| m < 0 = S.Stop
| otherwise = S.Yield m (mk (m - 1))
slowSource m = S.mapStepT delay (fastSource m) where
delay S.Stop = S.Stop
delay (S.Error err) = S.Error err
delay (S.Skip s) = S.Skip (delay s)
delay (S.Effect ms) = S.Effect (fmap delay ms)
delay (S.Yield x s) = S.Effect $
S.Yield x (delay s) <$ threadDelay 1000000
app :: Application
app = serve api server
cli :: Client ClientM FastAPI
cli :<|> _ :<|> _ :<|> _ = client api
main :: IO ()
main = do
args <- getArgs
case args of
("server":_) -> do
putStrLn "Starting cookbook-basic-streaming at http://localhost:8000"
port <- fromMaybe 8000 . (>>= readMaybe) <$> lookupEnv "PORT"
Warp.run port app
("client":ns:_) -> do
n <- maybe (fail $ "not a number: " ++ ns) pure $ readMaybe ns
mgr <- newManager defaultManagerSettings
burl <- parseBaseUrl "http://localhost:8000/"
withClientM (cli n) (mkClientEnv mgr burl) $ \me -> case me of
Left err -> print err
Right src -> do
x <- S.unSourceT src (go (0 :: Int))
print x
where
go !acc S.Stop = return acc
go !acc (S.Error err) = print err >> return acc
go !acc (S.Skip s) = go acc s
go !acc (S.Effect ms) = ms >>= go acc
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 "time curl -H 'Accept: application/json' localhost:8000/slow/5"
```

View file

@ -1,28 +0,0 @@
cabal-version: 2.2
name: cookbook-basic-streaming
version: 2.1
synopsis: Streaming in servant without streaming libs
homepage: http://docs.servant.dev/
license: BSD-3-Clause
license-file: ../../../servant/LICENSE
author: Servant Contributors
maintainer: haskell-servant-maintainers@googlegroups.com
build-type: Simple
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
executable cookbook-basic-streaming
main-is: Streaming.lhs
build-tool-depends: markdown-unlit:markdown-unlit
default-language: Haskell2010
ghc-options: -Wall -pgmL markdown-unlit -threaded -rtsopts
hs-source-dirs: .
build-depends: base >= 4.8 && <5
, aeson
, bytestring
, servant
, servant-server
, servant-client
, http-client
, wai
, warp

View file

@ -1,22 +0,0 @@
packages:
basic-auth/
curl-mock/
db-mysql-basics/
db-sqlite-simple/
db-postgres-pool/
using-custom-monad/
jwt-and-basic-auth/
hoist-server-with-context/
file-upload/
structuring-apis/
https/
pagination/
sentry/
testing/
open-id-connect/
../../servant
../../servant-server
../../servant-client-core
../../servant-client
../../servant-docs
../../servant-foreign

View file

@ -1,205 +0,0 @@
# Generating mock curl calls
In this example we will generate curl requests with mock post data from a servant API.
This may be useful for testing and development purposes.
Especially post requests with a request body are tedious to send manually.
Also, we will learn how to use the servant-foreign library to generate stuff from servant APIs.
Language extensions and imports:
``` haskell
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}
import Control.Lens ((^.))
import Data.Aeson
import Data.Aeson.Text
import Data.Proxy (Proxy (Proxy))
import Data.Text (Text)
import Data.Text.Encoding (decodeUtf8)
import qualified Data.Text.IO as T.IO
import qualified Data.Text.Lazy as LazyT
import GHC.Generics
import Servant ((:<|>), (:>), Get, JSON,
Post, ReqBody)
import Servant.Foreign (Foreign, GenerateList,
HasForeign, HasForeignType, Req,
Segment, SegmentType (Cap, Static),
argName, listFromAPI, path,
reqBody, reqMethod, reqUrl, typeFor,
unPathSegment, unSegment,)
import Test.QuickCheck.Arbitrary
import Test.QuickCheck.Arbitrary.Generic
import Test.QuickCheck.Gen (generate)
import qualified Data.Text as T
```
Let's define our API:
``` haskell
type UserAPI = "users" :> Get '[JSON] [User]
:<|> "new" :> "user" :> ReqBody '[JSON] User :> Post '[JSON] ()
data User = User
{ name :: String
, age :: Int
, email :: String
} deriving (Eq, Show, Generic)
instance Arbitrary User where
arbitrary = genericArbitrary
shrink = genericShrink
instance ToJSON User
instance FromJSON User
```
Notice the `Arbitrary User` instance which we will later need to create mock data.
Also, the obligatory servant boilerplate:
``` haskell
api :: Proxy UserAPI
api = Proxy
```
## servant-foreign and the HasForeignType Class
Servant-foreign allows us to look into the API we designed.
The entry point is `listFromAPI` which takes three types and returns a list of endpoints:
``` haskell ignore
listFromAPI :: (HasForeign lang ftype api, GenerateList ftype (Foreign ftype api)) => Proxy lang -> Proxy ftype -> Proxy api -> [Req ftype]
```
This looks a bit confusing...
[Here](https://hackage.haskell.org/package/servant-foreign/docs/Servant-Foreign.html#t:HasForeign) is the documentation for the `HasForeign` typeclass.
We will not go into details here, but this allows us to create a value of type `ftype` for any type `a` in our API.
In our case we want to create a mock of every type `a`.
We create a new datatype that holds our mocked value. Well, not the mocked value itself. To mock it we need IO (random). So the promise of a mocked value after some IO is performed:
``` haskell
data NoLang
data Mocked = Mocked (IO Text)
```
Now, we create an instance of `HasForeignType` for `NoLang` and `Mocked` for every `a` that implements `ToJSON` and `Arbitrary`:
``` haskell
instance (ToJSON a, Arbitrary a) => HasForeignType NoLang Mocked a where
typeFor _ _ _ =
Mocked (genText (Proxy :: Proxy a))
```
What does `genText` do? It generates an arbitrary value of type `a` and encodes it as text. (And does some lazy to non-lazy text transformation we do not care about):
``` haskell
genText :: (ToJSON a, Arbitrary a) => Proxy a -> IO Text
genText p =
fmap (\v -> LazyT.toStrict $ encodeToLazyText v) (genArb p)
genArb :: Arbitrary a => Proxy a -> IO a
genArb _ =
generate arbitrary
```
### Generating curl calls for every endpoint
Everything is prepared now and we can start generating some curl calls.
``` haskell
generateCurl :: (GenerateList Mocked (Foreign Mocked api), HasForeign NoLang Mocked api)
=> Proxy api
-> Text
-> IO Text
generateCurl p host =
fmap T.unlines body
where
body = mapM (generateEndpoint host)
$ listFromAPI (Proxy :: Proxy NoLang) (Proxy :: Proxy Mocked) p
```
First, `listFromAPI` gives us a list of `Req Mocked`. Each `Req` describes one endpoint from the API type.
We generate a curl call for each of them using the following helper.
``` haskell
generateEndpoint :: Text -> Req Mocked -> IO Text
generateEndpoint host req =
case maybeBody of
Just body ->
body >>= \b -> return $ T.intercalate " " [ "curl", "-X", method, "-d", "'" <> b <> "'"
, "-H 'Content-Type: application/json'", host <> "/" <> url ]
Nothing ->
return $ T.intercalate " " [ "curl", "-X", method, host <> "/" <> url ]
where
method = decodeUtf8 $ req ^. reqMethod
url = T.intercalate "/" $ map segment (req ^. reqUrl . path)
maybeBody = fmap (\(Mocked io) -> io) (req ^. reqBody)
```
`servant-foreign` offers a multitude of lenses to be used with `Req`-values.
`reqMethod` gives us a straigthforward `Network.HTTP.Types.Method`, `reqUrl` the url part and so on.
Just take a look at [the docs](https://hackage.haskell.org/package/servant-foreign/docs/Servant-Foreign.html).
But how do we get our mocked json string? This seems to be a bit to short to be true:
``` haskell ignore
maybeBody = fmap (\(Mocked io) -> io) (req ^. reqBody)
```
But it is that simple!
The docs say `reqBody` gives us a `Maybe f`. What is `f`, you ask? As defined in `generateCurl`, `f` is `Mocked` and contains a `IO Text`. How is this `Mocked` value created? The `HasForeignType::typeFor` does it!
Of course only if the endpoint has a request body.
Some (incomplete) code for url segments:
``` haskell
segment :: Segment Mocked -> Text
segment seg =
case unSegment seg of
Static p ->
unPathSegment p
Cap arg ->
-- Left as exercise for the reader: Mock args in the url
unPathSegment $ arg ^. argName
```
And now, lets hook it all up in our main function:
``` haskell
main :: IO ()
main =
generateCurl api "localhost:8081" >>= T.IO.putStrLn
```
Done:
``` curl
curl -X GET localhost:8081/users
curl -X POST -d '{"email":"wV򝣀_b򆎘:z񁊞򲙲!(3DM V","age":10,"name":"=|W"}' -H 'Content-Type: application/json' localhost:8081/new/user
```
This is of course no complete curl call mock generator, many things including path arguments are missing.
But it correctly generates mock calls for simple POST requests.
Also, we now know how to use `HasForeignType` and `listFromAPI` to generate anything we want.

View file

@ -1,29 +0,0 @@
cabal-version: 2.2
name: cookbook-curl-mock
version: 0.1
synopsis: Generate curl mock requests cookbook example
homepage: http://docs.servant.dev
license: BSD-3-Clause
license-file: ../../../servant/LICENSE
author: Servant Contributors
maintainer: haskell-servant-maintainers@googlegroups.com
build-type: Simple
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
executable cookbock-curl-mock
if impl(ghc >= 9.2)
-- generic-arbitrary is incompatible
buildable: False
main-is: CurlMock.lhs
build-depends: base == 4.*
, aeson
, lens
, text
, servant
, servant-server
, servant-foreign
, QuickCheck
, generic-arbitrary
default-language: Haskell2010
ghc-options: -Wall -pgmL markdown-unlit
build-tool-depends: markdown-unlit:markdown-unlit

View file

@ -1,189 +0,0 @@
# Customizing errors from Servant
Servant handles a lot of parsing and validation of the input request. When it can't parse something: query
parameters, URL parts or request body, it will return appropriate HTTP codes like 400 Bad Request.
These responses will contain the error message in their body without any formatting. However, it is often
desirable to be able to provide custom formatting for these error messages, for example, to wrap them in JSON.
Recently Servant got a way to add such formatting. This Cookbook chapter demonstrates how to use it.
Extensions and imports:
```haskell
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
import Data.Aeson
import Data.Proxy
import Data.Text
import GHC.Generics
import Network.Wai
import Network.Wai.Handler.Warp
import Servant
import Data.String.Conversions
(cs)
import Servant.API.ContentTypes
```
The API (from `greet.hs` example in Servant sources):
```haskell
-- | A greet message data type
newtype Greet = Greet { _msg :: Text }
deriving (Generic, Show)
instance FromJSON Greet
instance ToJSON Greet
-- API specification
type TestApi =
-- GET /hello/:name?capital={true, false} returns a Greet as JSON
"hello" :> Capture "name" Text :> QueryParam "capital" Bool :> Get '[JSON] Greet
-- POST /greet with a Greet as JSON in the request body,
-- returns a Greet as JSON
:<|> "greet" :> ReqBody '[JSON] Greet :> Post '[JSON] Greet
-- DELETE /greet/:greetid
:<|> "greet" :> Capture "greetid" Text :> Delete '[JSON] NoContent
testApi :: Proxy TestApi
testApi = Proxy
-- Server-side handlers.
--
-- There's one handler per endpoint, which, just like in the type
-- that represents the API, are glued together using :<|>.
--
-- Each handler runs in the 'Handler' monad.
server :: Server TestApi
server = helloH :<|> postGreetH :<|> deleteGreetH
where helloH name Nothing = helloH name (Just False)
helloH name (Just False) = return . Greet $ "Hello, " <> name
helloH name (Just True) = return . Greet . toUpper $ "Hello, " <> name
postGreetH greet = return greet
deleteGreetH _ = return NoContent
```
## Error formatters
`servant-server` provides an `ErrorFormatter` type to specify how the error message will be
formatted. A formatter is just a function accepting three parameters:
- `TypeRep` from `Data.Typeable`: this is a runtime representation of the type of the combinator
(like `Capture` or `ReqBody`) that generated the error. It can be used to display its name (with
`show`) or even dynamically dispatch on the combinator type. See the docs for `Data.Typeable` and
`Type.Reflection` modules.
- `Request`: full information for the request that led to the error.
- `String`: specific error message from the combinator.
The formatter is expected to produce a `ServerError` which will be returned from the handler.
Additionally, there is `NotFoundErrorFormatter`, which accepts only `Request` and can customize the
error in case when no route can be matched (HTTP 404).
Let's make two formatters. First one will wrap our error in a JSON:
```json
{
"error": "ERROR MESSAGE",
"combinator": "NAME OF THE COMBINATOR"
}
```
Additionally, this formatter will examine the `Accept` header of the request and generate JSON
message only if client can accept it.
```haskell
customFormatter :: ErrorFormatter
customFormatter tr req err =
let
-- aeson Value which will be sent to the client
value = object ["combinator" .= show tr, "error" .= err]
-- Accept header of the request
accH = getAcceptHeader req
in
-- handleAcceptH is Servant's function that checks whether the client can accept a
-- certain message type.
-- In this case we call it with "Proxy '[JSON]" argument, meaning that we want to return a JSON.
case handleAcceptH (Proxy :: Proxy '[JSON]) accH value of
-- If client can't handle JSON, we just return the body the old way
Nothing -> err400 { errBody = cs err }
-- Otherwise, we return the JSON formatted body and set the "Content-Type" header.
Just (ctypeH, body) -> err400
{ errBody = body
, errHeaders = [("Content-Type", cs ctypeH)]
}
notFoundFormatter :: NotFoundErrorFormatter
notFoundFormatter req =
err404 { errBody = cs $ "Not found path: " <> rawPathInfo req }
```
If you don't need to react to the `Accept` header, you can just unconditionally return the JSON like
this (with `encode` from `Data.Aeson`):
```
err400
{ errBody = encode body
, errHeaders = [("Content-Type", "application/json")]
}
```
## Passing formatters to Servant
Servant uses the Context to configure formatters. You only need to add a value of type
`ErrorFormatters` to your context. This is a record with the following fields:
- `bodyParserErrorFormatter :: ErrorFormatter`
- `urlParseErrorFormatter :: ErrorFormatter`
- `headerParseErrorFormatter :: ErrorFormatter`
- `notFoundErrorFormatter :: NotFoundErrorFormatter`
Default formatters are exported as `defaultErrorFormatters`, so you can use record update syntax to
set the only ones you need:
```haskell
customFormatters :: ErrorFormatters
customFormatters = defaultErrorFormatters
{ bodyParserErrorFormatter = customFormatter
, notFoundErrorFormatter = notFoundFormatter
}
```
And at last, use `serveWithContext` to run your server as usual:
```haskell
app :: Application
app = serveWithContext testApi (customFormatters :. EmptyContext) server
main :: IO ()
main = run 8000 app
```
Now if we try to request something with a wrong body, we will get a nice error:
```
$ http -j POST localhost:8000/greet 'foo=bar'
HTTP/1.1 400 Bad Request
Content-Type: application/json;charset=utf-8
Date: Fri, 17 Jul 2020 13:34:18 GMT
Server: Warp/3.3.12
Transfer-Encoding: chunked
{
"combinator": "ReqBody'",
"error": "Error in $: parsing Main.Greet(Greet) failed, key \"_msg\" not found"
}
```
Notice the `Content-Type` header set by our combinator.

View file

@ -1,25 +0,0 @@
cabal-version: 2.2
name: cookbook-custom-errors
version: 0.1
synopsis: Return custom error messages from combinators
homepage: http://docs.servant.dev
license: BSD-3-Clause
license-file: ../../../servant/LICENSE
author: Servant Contributors
maintainer: haskell-servant-maintainers@googlegroups.com
build-type: Simple
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
executable cookbook-custom-errors
main-is: CustomErrors.lhs
build-depends: base == 4.*
, aeson
, servant
, servant-server
, string-conversions
, text
, wai
, warp
default-language: Haskell2010
ghc-options: -Wall -pgmL markdown-unlit
build-tool-depends: markdown-unlit:markdown-unlit

View file

@ -1,236 +0,0 @@
# Overview
This doc will walk through a single-module implementation of a servant API connecting to a MySQL database. It will also include some basic CRUD operations.
Once you can wrap your head around this implementation, understanding more complex features like resource pools would be beneficial next steps.
The only *prerequisite* is that you have a MySQL database open on port 3306 of your machine. Docker is an easy way to manage this.
## Setup
- The mysql database should be up and running on 127.0.0.1:3306
- Our API will be exposed on localhost:8080
## REST actions available
*Get all people*
```
/people GET
```
*Get person by ID*
```
/people/:id GET
```
*Insert a new person*
```
/people POST
{
"name": "NewName",
"age": 24
}
```
*Delete a person*
```
/people/:id DELETE
```
## Other notes
At the time of writing this issue may occur when building your project:
```
setup: Missing dependencies on foreign libraries:
* Missing (or bad) C libraries: ssl, crypto
```
If using stack, this can be fixed by adding the following lines to your `stack.yaml`:
```
extra-include-dirs:
- /usr/local/opt/openssl/include
extra-lib-dirs:
- /usr/local/opt/openssl/lib
```
Or for cabal, running your builds with these configurations passed as options.
## Implementation: Main.hs
Let's jump in:
```haskell
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
module Lib where
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Logger (NoLoggingT (..))
import Control.Monad.Trans.Reader (runReaderT)
import Control.Monad.Trans.Resource (ResourceT, runResourceT)
import Data.Aeson as JSON
import Data.Int (Int64 (..))
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Database.Persist
import Database.Persist.MySQL (ConnectInfo (..),
SqlBackend (..),
defaultConnectInfo, fromSqlKey, runMigration,
runSqlPool, toSqlKey, withMySQLConn)
import Database.Persist.Sql (SqlPersistT, runSqlConn)
import Database.Persist.TH (mkMigrate, mkPersist,
persistLowerCase, share,
sqlSettings)
import Database.Persist.Types (PersistValue(PersistInt64))
import Servant (Handler, throwError)
import GHC.Generics
import Network.Wai
import Network.Wai.Handler.Warp
import Servant
import Servant.API
import System.Environment (getArgs)
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
Person json
Id Int Primary Unique
name Text
age Text
deriving Eq Show Generic
|]
type Api =
"person" :> Get '[JSON] [Person]
:<|> "person" :> Capture "id" Int :> Get '[JSON] Person
:<|> "person" :> Capture "id" Int :> Delete '[JSON] ()
:<|> "person" :> ReqBody '[JSON] Person :> Post '[JSON] Person
apiProxy :: Proxy Api
apiProxy = Proxy
app :: Application
app = serve apiProxy server
-- Run a database operation, and lift the result into a Handler.
-- This minimises usage of IO operations in other functions
runDB :: SqlPersistT (ResourceT (NoLoggingT IO)) a -> Handler a
runDB a = liftIO $ runNoLoggingT $ runResourceT $ withMySQLConn connInfo $ runSqlConn a
-- Change these out to suit your local setup
connInfo :: ConnectInfo
connInfo = defaultConnectInfo { connectHost = "127.0.0.1", connectUser = "root", connectPassword = "abcd", connectDatabase = "test-database" }
doMigration :: IO ()
doMigration = runNoLoggingT $ runResourceT $ withMySQLConn connInfo $ runReaderT $ runMigration migrateAll
server :: Server Api
server =
personGET :<|>
personGETById :<|>
personDELETE :<|>
personPOST
where
personGET = selectPersons
personGETById id = selectPersonById id
personDELETE id = deletePerson id
personPOST personJson = createPerson personJson
selectPersons :: Handler [Person]
selectPersons = do
personList <- runDB $ selectList [] []
return $ map (\(Entity _ u) -> u) personList
selectPersonById :: Int -> Handler Person
selectPersonById id = do
sqlResult <- runDB $ get $ PersonKey id
case sqlResult of
Just person -> return person
Nothing -> throwError err404 { errBody = JSON.encode "Person with ID not found." }
createPerson :: Person -> Handler Person
createPerson person = do
attemptCreate <- runDB $ insert person
case attemptCreate of
PersonKey k -> return person
_ -> throwError err503 { errBody = JSON.encode "Could not create Person." }
deletePerson :: Int -> Handler ()
deletePerson id = do runDB $ delete $ PersonKey id
startApp :: IO ()
startApp = do
args <- getArgs
let arg1 = if not (null args) then Just (head args) else Nothing
case arg1 of
Just "migrate" -> doMigration
_ -> run 8080 app
```
## Sample requests
Assuming that you have the db running and have first run `stack exec run migrate`, the following sample requests will test your API:
*Create a person*
```bash
curl -X POST \
http://localhost:8080/person \
-H 'Accept: */*' \
-H 'Accept-Encoding: gzip, deflate' \
-H 'Cache-Control: no-cache' \
-H 'Connection: keep-alive' \
-H 'Content-Length: 62' \
-H 'Content-Type: application/json' \
-H 'Host: localhost:8080' \
-H 'cache-control: no-cache' \
-d '{
"name": "Jake",
"age": "25"
}'
```
*Get all persons*
```bash
curl -X GET \
http://localhost:8080/person \
-H 'Accept: */*' \
-H 'Accept-Encoding: gzip, deflate' \
-H 'Cache-Control: no-cache' \
-H 'Connection: keep-alive' \
-H 'Content-Length: 33' \
-H 'Content-Type: application/json' \
-H 'Host: localhost:8080' \
-H 'cache-control: no-cache'
```
*Get person by ID*
```bash
curl -X GET \
http://localhost:8080/person/1 \
-H 'Accept: */*' \
-H 'Accept-Encoding: gzip, deflate' \
-H 'Cache-Control: no-cache' \
-H 'Connection: keep-alive' \
-H 'Content-Type: application/json' \
-H 'Host: localhost:8080' \
-H 'cache-control: no-cache'
```

View file

@ -1,40 +0,0 @@
cabal-version: 2.2
name: mysql-basics
version: 0.1.0.0
synopsis: Simple MySQL API cookbook example
homepage: http://docs.servant.dev/
license: BSD-3-Clause
license-file: ../../../servant/LICENSE
author: Servant Contributors
maintainer: haskell-servant-maintainers@googlegroups.com
build-type: Simple
executable run
hs-source-dirs: .
main-is: MysqlBasics.hs
ghc-options: -threaded -rtsopts -with-rtsopts=-N
build-depends: aeson
, base
, bytestring
, http-client
, monad-logger
, mysql-simple
, persistent
, persistent-mysql
, persistent-template
, resource-pool
, resourcet
, servant
, servant-client
, servant-server
, text
, transformers
, wai
, warp
default-language: Haskell2010
ghc-options: -Wall -pgmL markdown-unlit
build-tool-depends: markdown-unlit:markdown-unlit
source-repository head
type: git
location: https://github.com/githubuser/mysql-basics

View file

@ -1,143 +0,0 @@
# PostgreSQL connection pool
Let's see how we can write a simple web application that uses a
[PostgreSQL](https://www.postgresql.org/) database to store simple textual
messages, just like in the SQLite cookbook recipe. The main difference,
besides the database technology, is that in this example we will be using
a pool of connections to talk to the database server. The pool abstraction
will be provided by the
[resource-pool](https://hackage.haskell.org/package/resource-pool) library.
As usual, we start with a little bit of throat clearing.
``` haskell
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeOperators #-}
import Data.ByteString (ByteString)
import Control.Concurrent
import Control.Exception (bracket)
import Control.Monad.IO.Class
import Data.Pool
import Database.PostgreSQL.Simple
import Network.HTTP.Client (newManager, defaultManagerSettings)
import Network.Wai.Handler.Warp
import Servant
import Servant.Client
type DBConnectionString = ByteString
```
We will only care about a single type here, the messages. We want to
be able to add a new one and retrieve them all, using two different
endpoints.
``` haskell
type Message = String
type API = ReqBody '[PlainText] Message :> Post '[JSON] NoContent
:<|> Get '[JSON] [Message]
api :: Proxy API
api = Proxy
```
We proceed with a simple function for creating a table
for holding our messages if it doesn't already exist, given
a PostgreSQL connection string.
``` haskell
initDB :: DBConnectionString -> IO ()
initDB connstr = bracket (connectPostgreSQL connstr) close $ \conn -> do
execute_ conn
"CREATE TABLE IF NOT EXISTS messages (msg text not null)"
return ()
```
Next, our server implementation. It will be parametrised (take as
argument) by the pool of database connections that handlers can use to
talk to the PostgreSQL database. The resource pool abstraction allows us
to flexibly set up a whole bunch of PostgreSQL connections tailored to our
needs and then to forget about it all by simply asking for a connection
using `withResource`.
The handlers are straightforward. One takes care of inserting a new
value in the database while the other fetches all messages and returns
them. We also provide a function for serving our web app given a PostgreSQL
connection pool, which simply calls servant-server's `serve` function.
``` haskell
server :: Pool Connection -> Server API
server conns = postMessage :<|> getMessages
where postMessage :: Message -> Handler NoContent
postMessage msg = do
liftIO . withResource conns $ \conn ->
execute conn
"INSERT INTO messages VALUES (?)"
(Only msg)
return NoContent
getMessages :: Handler [Message]
getMessages = fmap (map fromOnly) . liftIO $
withResource conns $ \conn ->
query_ conn "SELECT msg FROM messages"
runApp :: Pool Connection -> IO ()
runApp conns = run 8080 (serve api $ server conns)
```
We will also need a function for initialising our connection pool.
`resource-pool` is quite configurable, feel free to wander in
[its documentation](https://hackage.haskell.org/package/resource-pool)
to gain a better understanding of how it works and what the configuration
knobs are. I will be using some dummy values in this example.
``` haskell
initConnectionPool :: DBConnectionString -> IO (Pool Connection)
initConnectionPool connStr =
createPool (connectPostgreSQL connStr)
close
2 -- stripes
60 -- unused connections are kept open for a minute
10 -- max. 10 connections open per stripe
```
Let's finally derive some clients for our API and use them to
insert two messages and retrieve them in `main`, after setting up
our pool of database connections.
``` haskell
postMsg :: Message -> ClientM NoContent
getMsgs :: ClientM [Message]
postMsg :<|> getMsgs = client api
main :: IO ()
main = do
-- you could read this from some configuration file,
-- environment variable or somewhere else instead.
-- you will need to either change this connection string OR
-- set some environment variables (see
-- https://www.postgresql.org/docs/9.5/static/libpq-envars.html)
-- to point to a running PostgreSQL server for this example to work.
let connStr = ""
pool <- initConnectionPool connStr
initDB connStr
mgr <- newManager defaultManagerSettings
bracket (forkIO $ runApp pool) killThread $ \_ -> do
ms <- flip runClientM (mkClientEnv mgr (BaseUrl Http "localhost" 8080 "")) $ do
postMsg "hello"
postMsg "world"
getMsgs
print ms
```
This program prints `Right ["hello","world"]` the first time it is executed,
`Right ["hello","world","hello","world"]` the second time and so on.
You could alternatively have the handlers live in `ReaderT (Pool Connection)`
and access the pool using e.g `ask`, but this would be more complicated
than simply taking the pool as argument.
The entire source for this example is available as a cabal project
[here](https://github.com/haskell-servant/servant/tree/master/doc/cookbook/db-postgres-pool).

View file

@ -1,32 +0,0 @@
cabal-version: 2.2
name: cookbook-db-postgres-pool
version: 0.1
synopsis: Simple PostgreSQL connection pool cookbook example
homepage: http://docs.servant.dev/
license: BSD-3-Clause
license-file: ../../../servant/LICENSE
author: Servant Contributors
maintainer: haskell-servant-maintainers@googlegroups.com
build-type: Simple
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
executable cookbook-db-postgres-pool
main-is: PostgresPool.lhs
build-depends: base == 4.*
, bytestring >= 0.10
, text >= 1.2
, aeson >= 1.2
, servant
, servant-client
, servant-server
, warp >= 3.2
, wai >= 3.2
, http-types >= 0.12
, markdown-unlit >= 0.4
, http-client >= 0.5
, postgresql-simple >= 0.5
, resource-pool >= 0.2
, transformers
default-language: Haskell2010
ghc-options: -Wall -pgmL markdown-unlit
build-tool-depends: markdown-unlit:markdown-unlit

View file

@ -1,100 +0,0 @@
# SQLite database
Let's see how we can write a simple web application that uses an
[SQLite](https://www.sqlite.org/) database to store simple textual
messages. As usual, we start with a little bit of throat clearing.
``` haskell
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeOperators #-}
import Control.Concurrent
import Control.Exception (bracket)
import Control.Monad.IO.Class
import Database.SQLite.Simple
import Network.HTTP.Client (newManager, defaultManagerSettings)
import Network.Wai.Handler.Warp
import Servant
import Servant.Client
```
We will only care about a single type here, the messages. We want to
be able to add a new one and retrieve them all, using two different
endpoints.
``` haskell
type Message = String
type API = ReqBody '[PlainText] Message :> Post '[JSON] NoContent
:<|> Get '[JSON] [Message]
api :: Proxy API
api = Proxy
```
We proceed with a simple function for creating a table
for holding our messages if it doesn't already exist.
``` haskell
initDB :: FilePath -> IO ()
initDB dbfile = withConnection dbfile $ \conn ->
execute_ conn
"CREATE TABLE IF NOT EXISTS messages (msg text not null)"
```
Next, our server implementation. It will be parametrised (take as an
argument) by the name of the file that contains our SQLite database.
The handlers are straightforward. One takes care of inserting a new
value in the database while the other fetches all messages and returns
them. We also provide a function for serving our web app given an
SQLite database file, which simply calls servant-server's `serve` function.
``` haskell
server :: FilePath -> Server API
server dbfile = postMessage :<|> getMessages
where postMessage :: Message -> Handler NoContent
postMessage msg = do
liftIO . withConnection dbfile $ \conn ->
execute conn
"INSERT INTO messages VALUES (?)"
(Only msg)
return NoContent
getMessages :: Handler [Message]
getMessages = fmap (map fromOnly) . liftIO $
withConnection dbfile $ \conn ->
query_ conn "SELECT msg FROM messages"
runApp :: FilePath -> IO ()
runApp dbfile = run 8080 (serve api $ server dbfile)
```
Let's also derive some clients for our API and use them to
insert two messages and retrieve them in `main`.
``` haskell
postMsg :: Message -> ClientM NoContent
getMsgs :: ClientM [Message]
postMsg :<|> getMsgs = client api
main :: IO ()
main = do
-- you could read this from some configuration file,
-- environment variable or somewhere else instead.
let dbfile = "test.db"
initDB dbfile
mgr <- newManager defaultManagerSettings
bracket (forkIO $ runApp dbfile) killThread $ \_ -> do
ms <- flip runClientM (mkClientEnv mgr (BaseUrl Http "localhost" 8080 "")) $ do
postMsg "hello"
postMsg "world"
getMsgs
print ms
```
This program prints `Right ["hello","world"]` the first time it is executed,
`Right ["hello","world","hello","world"]` the second time and so on.
The entire source for this example is available as a cabal project
[here](https://github.com/haskell-servant/servant/tree/master/doc/cookbook/db-sqlite-simple).

View file

@ -1,30 +0,0 @@
cabal-version: 2.2
name: cookbook-db-sqlite-simple
version: 0.1
synopsis: Simple SQLite DB cookbook example
homepage: http://docs.servant.dev/
license: BSD-3-Clause
license-file: ../../../servant/LICENSE
author: Servant Contributors
maintainer: haskell-servant-maintainers@googlegroups.com
build-type: Simple
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
executable cookbook-db-sqlite-simple
main-is: DBConnection.lhs
build-depends: base == 4.*
, text >= 1.2
, aeson >= 1.2
, servant
, servant-client
, servant-server
, warp >= 3.2
, wai >= 3.2
, http-types >= 0.12
, markdown-unlit >= 0.4
, http-client >= 0.5
, sqlite-simple >= 0.4.5.0
, transformers
default-language: Haskell2010
ghc-options: -Wall -pgmL markdown-unlit
build-tool-depends: markdown-unlit:markdown-unlit

View file

@ -1,140 +0,0 @@
# File Upload (`multipart/form-data`)
In this recipe, we will implement a web application
with a single endpoint that can process
`multipart/form-data` request bodies, which most
commonly come from HTML forms that allow file upload.
As usual, a bit of throat clearing.
``` haskell
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE OverloadedStrings #-}
import Control.Concurrent
import Control.Exception
import Control.Monad
import Control.Monad.IO.Class
import Data.Text.Encoding (encodeUtf8)
import Network.Socket (withSocketsDo)
import Network.HTTP.Client hiding (Proxy)
import Network.HTTP.Client.MultipartFormData
import Network.Wai.Handler.Warp
import Servant
import Servant.Multipart
import qualified Data.ByteString.Lazy as LBS
```
Our API consists in a single `POST` endpoint at `/`
that takes a `multipart/form-data` request body and
pretty-prints the data it got to stdout before returning `0`
(because why not).
``` haskell
type API = MultipartForm Mem (MultipartData Mem) :> Post '[JSON] Integer
api :: Proxy API
api = Proxy
```
Because of some technicalities, multipart form data is not
represented as a good old content type like `JSON` in servant,
that one could use with `ReqBody`, but instead is its own
dedicated `ReqBody`-like combinator named
[`MultiPartForm`](https://hackage.haskell.org/package/servant-multipart-0.11/docs/Servant-Multipart.html#t:MultipartForm).
This combinator takes two parameters. The first one is the
"backend" to use. Currently, you only have the choice between
`Mem` and `Tmp`. The former loads the entire input in memory,
even the uploaded files, while `Tmp` will stream uploaded
files to some temporary directory.
The second parameter is the type you want the multipart data
to be decoded to. Indeed there is a `FromMultipart` class that
allows you to specify how to decode multipart form data from
`MultipartData` to a custom type of yours. Here we use the
trivial "decoding" to `MultipartData` itself, and simply
will get our hands on the raw input. If you want to use
a type of yours, see the documentation for
[`FromMultipart`](https://hackage.haskell.org/package/servant-multipart-0.11/docs/Servant-Multipart.html#t:FromMultipart).
Our only request handler has type `MultipartData Mem -> Handler Integer`.
All it does is list the textual and file inputs that
were sent in the multipart request body. The textual
inputs are in the `inputs` field while the file inputs
are in the `files` field of `multipartData`.
``` haskell
-- MultipartData consists in textual inputs,
-- accessible through its "inputs" field, as well
-- as files, accessible through its "files" field.
upload :: Server API
upload multipartData = do
liftIO $ do
putStrLn "Inputs:"
forM_ (inputs multipartData) $ \input ->
putStrLn $ " " ++ show (iName input)
++ " -> " ++ show (iValue input)
forM_ (files multipartData) $ \file -> do
let content = fdPayload file
putStrLn $ "Content of " ++ show (fdFileName file)
LBS.putStr content
return 0
startServer :: IO ()
startServer = run 8080 (serve api upload)
```
Finally, a main function that brings up our server and
sends some test request with `http-client` (and not
servant-client this time, as servant-multipart does not
yet have support for client generation).
``` haskell
main :: IO ()
main = withSocketsDo . bracket (forkIO startServer) killThread $ \_threadid -> do
-- we fork the server in a separate thread and send a test
-- request to it from the main thread.
manager <- newManager defaultManagerSettings
req <- parseRequest "http://localhost:8080/"
resp <- flip httpLbs manager =<< formDataBody form req
print resp
where form =
[ partBS "title" "World"
, partBS "text" $ encodeUtf8 "Hello"
, partFileSource "file" "./README.md"
]
```
If you run this, you should get:
``` bash
$ cabal new-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:
"title" -> "World"
"text" -> "Hello"
Content of "README.md"
# servant - A Type-Level Web DSL
![servant](https://raw.githubusercontent.com/haskell-servant/servant/master/servant.png)
## Getting Started
We have a [tutorial](http://docs.servant.dev/en/stable/tutorial/index.html) that
introduces the core features of servant. After this article, you should be able
to write your first servant webservices, learning the rest from the haddocks'
examples.
[...]
Response {responseStatus = Status {statusCode = 200, statusMessage = "OK"}, responseVersion = HTTP/1.1, responseHeaders = [("Transfer-Encoding","chunked"),("Date","Fri, 08 Dec 2017 16:50:14 GMT"),("Server","Warp/3.2.13"),("Content-Type","application/json;charset=utf-8")], responseBody = "0", responseCookieJar = CJ {expose = []}, responseClose' = ResponseClose}
```
As usual, the code for this recipe is available in a cabal project
[here](https://github.com/haskell-servant/servant/tree/master/doc/cookbook/file-upload).

View file

@ -1,30 +0,0 @@
cabal-version: 2.2
name: cookbook-file-upload
version: 0.1
synopsis: File upload cookbook example
homepage: http://docs.servant.dev/
license: BSD-3-Clause
license-file: ../../../servant/LICENSE
author: Servant Contributors
maintainer: haskell-servant-maintainers@googlegroups.com
build-type: Simple
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
executable cookbook-file-upload
main-is: FileUpload.lhs
build-depends: base == 4.*
, text >= 1.2
, mtl >= 2.1
, network >= 2.6
, bytestring >= 0.10
, servant
, servant-server
, servant-multipart
, transformers
, warp >= 3.2
, wai >= 3.2
, markdown-unlit >= 0.4
, http-client >= 0.5
default-language: Haskell2010
ghc-options: -Wall -pgmL markdown-unlit
build-tool-depends: markdown-unlit:markdown-unlit

View file

@ -1,141 +0,0 @@
# Using generics
```haskell
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeOperators #-}
module Main (main, api, getLink, routesLinks, cliGet) where
import Control.Exception (throwIO)
import Control.Monad.Trans.Reader (ReaderT, runReaderT)
import Data.Proxy (Proxy (..))
import Network.Wai.Handler.Warp (run)
import System.Environment (getArgs)
import Servant
import Servant.Client
import Servant.API.Generic
import Servant.Client.Generic
import Servant.Server.Generic
```
The usage is simple, if you only need a collection of routes.
First you define a record with field types prefixed by a parameter `route`:
```haskell
data Routes route = Routes
{ _get :: route :- Capture "id" Int :> Get '[JSON] String
, _put :: route :- ReqBody '[JSON] Int :> Put '[JSON] Bool
}
deriving (Generic)
```
Then we'll use this data type to define API, links, server and client.
## API
You can get a `Proxy` of the API using `genericApi`:
```haskell
api :: Proxy (ToServantApi Routes)
api = genericApi (Proxy :: Proxy Routes)
```
It's recommended to use `genericApi` function, as then you'll get
better error message, for example if you forget to `derive Generic`.
## Links
The clear advantage of record-based generics approach, is that
we can get safe links very conveniently. We don't need to define endpoint types,
as field accessors work as proxies:
```haskell
getLink :: Int -> Link
getLink = fieldLink _get
```
We can also get all links at once, as a record:
```haskell
routesLinks :: Routes (AsLink Link)
routesLinks = allFieldLinks
```
## Client
Even more power starts to show when we generate a record of client functions.
Here we use `genericClientHoist` function, which lets us simultaneously
hoist the monad, in this case from `ClientM` to `IO`.
```haskell
cliRoutes :: Routes (AsClientT IO)
cliRoutes = genericClientHoist
(\x -> runClientM x env >>= either throwIO return)
where
env = error "undefined environment"
cliGet :: Int -> IO String
cliGet = _get cliRoutes
```
## Server
Finally, probably the most handy usage: we can convert record of handlers into
the server implementation:
```haskell
record :: Routes AsServer
record = Routes
{ _get = return . show
, _put = return . odd
}
app :: Application
app = genericServe record
main :: IO ()
main = do
args <- getArgs
case args of
("run":_) -> do
putStrLn "Starting cookbook-generic at http://localhost:8000"
run 8000 app
-- see this cookbook below for custom-monad explanation
("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"
```
## Using generics together with a custom monad
If your app uses a custom monad, here's how you can combine it with
generics.
```haskell
data AppCustomState =
AppCustomState
type AppM = ReaderT AppCustomState Handler
apiMyMonad :: Proxy (ToServantApi Routes)
apiMyMonad = genericApi (Proxy :: Proxy Routes)
getRouteMyMonad :: Int -> AppM String
getRouteMyMonad = return . show
putRouteMyMonad :: Int -> AppM Bool
putRouteMyMonad = return . odd
recordMyMonad :: Routes (AsServerT AppM)
recordMyMonad = Routes {_get = getRouteMyMonad, _put = putRouteMyMonad}
-- natural transformation
nt :: AppCustomState -> AppM a -> Handler a
nt s x = runReaderT x s
appMyMonad :: AppCustomState -> Application
appMyMonad state = genericServeT (nt state) recordMyMonad

View file

@ -1,25 +0,0 @@
cabal-version: 2.2
name: cookbook-generic
version: 0.1
synopsis: Using custom monad to pass a state between handlers
homepage: http://docs.servant.dev/
license: BSD-3-Clause
license-file: ../../../servant/LICENSE
author: Servant Contributors
maintainer: haskell-servant-maintainers@googlegroups.com
build-type: Simple
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
executable cookbook-using-custom-monad
main-is: Generic.lhs
build-depends: base == 4.*
, servant
, servant-client
, servant-client-core
, servant-server
, base-compat
, warp >= 3.2
, transformers >= 0.3
default-language: Haskell2010
ghc-options: -Wall -pgmL markdown-unlit
build-tool-depends: markdown-unlit:markdown-unlit >= 0.4

View file

@ -1,413 +0,0 @@
# Hoist Server With Context for Custom Monads
In this example we'll combine some of the patterns we've seen in other examples
in order to demonstrate using a custom monad with Servant's `Context` and the function
`hoistServerWithContext`.
`hoistServerWithContext` is a pattern you may encounter if you are trying to use a library such as
[servant-auth-server](https://hackage.haskell.org/package/servant-auth-server) along
with your own custom monad.
In this example, our custom monad will be based on the commonly used `ReaderT env IO a` stack.
We'll create an `AppCtx` to represent our `env` and include some logging utilities as well as
other variables we'd like to have available.
In addition, in order to demonstrate a custom `Context`, we'll also include authentication in
our example. As noted previously (in [jwt-and-basic-auth](../jwt-and-basic-auth/JWTAndBasicAuth.lhs)),
while basic authentication comes with Servant itself,
[servant-auth](https://hackage.haskell.org/package/servant-auth) and
[servant-auth-server](https://hackage.haskell.org/package/servant-auth-server)
packages are needed for JWT-based authentication.
Finally, we're going to use [fast-logger](http://hackage.haskell.org/package/fast-logger)
for our logging example below.
This recipe uses the following ingredients:
```haskell
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
import Prelude ()
import Prelude.Compat
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Reader
import Data.Aeson
import Data.Default
import Data.Proxy
import Data.Text
import Data.Time.Clock ( UTCTime, getCurrentTime )
import GHC.Generics
import Network.Wai (Middleware)
import Network.Wai.Handler.Warp as Warp
import Network.Wai.Middleware.RequestLogger
import Network.Wai.Middleware.RequestLogger.JSON
import Servant as S
import Servant.Auth as SA
import Servant.Auth.Server as SAS
import System.Log.FastLogger ( ToLogStr(..)
, LoggerSet
, defaultBufSize
, newStdoutLoggerSet
, flushLogStr
, pushLogStrLn )
port :: Int
port = 3001
```
## Custom Monad
Let's say we'd like to create a custom monad based on `ReaderT env` in order to hold
access to a config object as well as some logging utilities.
With that, we could define an `AppCtx` and `AppM` like this:
```haskell
type AppM = ReaderT AppCtx Handler
data AppCtx = AppCtx {
_getConfig :: SiteConfig
, _getLogger :: LoggerSet
}
data SiteConfig = SiteConfig {
environment :: !Text
, version :: !Text
, adminUsername :: !Text
, adminPasswd :: !Text
} deriving (Generic, Show)
```
This `SiteConfig` is a simple example: it refers to our deployment environment as well as an
application version. For instance, we may do something different based on the environment our app is
deployed into. When emitting log messages, we may want to include information about
the deployed version of our application.
In addition, we're going to identify a single admin user in our config and use
that definition to authenticate requests inside our handlers. This is not too
flexible (and probably not too secure...), but it works as a simple example.
## Logging
A common contemporary pattern is to emit log messages as JSON for later ingestion
into a database like Elasticsearch.
To emit JSON log messages, we'll create a `LogMessage` object and make it so we can turn it
into a JSON-encoded `LogStr` (a type from `fast-logger`).
```haskell
data LogMessage = LogMessage {
message :: !Text
, timestamp :: !UTCTime
, level :: !Text
, lversion :: !Text
, lenvironment :: !Text
} deriving (Eq, Show, Generic)
instance FromJSON LogMessage
instance ToJSON LogMessage where
toEncoding = genericToEncoding defaultOptions
instance ToLogStr LogMessage where
toLogStr = toLogStr . encode
```
Eventually, when we'd like to emit a log message inside one of our Handlers, it'll look like this:
```haskell
sampleHandler :: AppM LogMessage
sampleHandler = do
config <- asks _getConfig
logset <- asks _getLogger
tstamp <- liftIO getCurrentTime
let logMsg = LogMessage { message = "let's do some logging!"
, timestamp = tstamp
, level = "info"
, lversion = version config
, lenvironment = environment config
}
-- emit log message
liftIO $ pushLogStrLn logset $ toLogStr logMsg
-- return handler result (for simplicity, result is also a LogMessage)
pure logMsg
```
## Authentication
To demonstrate the other part of this recipe, we are going to use a simple
representation of a user, someone who may have access to an admin section of our site:
```haskell
data AdminUser = AdminUser { name :: Text }
deriving (Eq, Show, Read, Generic)
```
The following instances are needed for JWT:
```haskell
instance ToJSON AdminUser
instance FromJSON AdminUser
instance SAS.ToJWT AdminUser
instance SAS.FromJWT AdminUser
```
## API
Now we can define our API.
We'll have an `admin` endpoint and a `login` endpoint that takes a `LoginForm`:
```haskell
type AdminApi =
"admin" :> Get '[JSON] LogMessage
type LoginApi =
"login"
:> ReqBody '[JSON] LoginForm
:> Post '[JSON] (Headers '[ Header "Set-Cookie" SetCookie, Header "Set-Cookie" SetCookie] LogMessage)
data LoginForm = LoginForm {
username :: Text
, password :: Text
} deriving (Eq, Show, Generic)
instance ToJSON LoginForm
instance FromJSON LoginForm
```
We can combine both APIs into one like so:
```haskell
type AdminAndLogin auths = (SAS.Auth auths AdminUser :> AdminApi) :<|> LoginApi
```
## Server
When we define our server, we'll have to define handlers for the `AdminApi` and the `LoginApi` and
we'll have to supply `JWTSettings` and `CookieSettings` so our `login` handler can authenticate users:
```haskell
adminServer :: SAS.CookieSettings -> SAS.JWTSettings -> ServerT (AdminAndLogin auths) AppM
adminServer cs jwts = adminHandler :<|> loginHandler cs jwts
```
The `admin` route should receive an authenticated `AdminUser` as an argument
or it should return a `401`:
```haskell
adminHandler :: AuthResult AdminUser -> AppM LogMessage
adminHandler (SAS.Authenticated adminUser) = do
config <- asks _getConfig
logset <- asks _getLogger
tstamp <- liftIO getCurrentTime
let logMsg = LogMessage { message = "Admin User accessing admin: " <> name adminUser
, timestamp = tstamp
, level = "info"
, lversion = version config
, lenvironment = environment config
}
-- emit log message
liftIO $ pushLogStrLn logset $ toLogStr logMsg
-- return handler result (for simplicity, result is a LogMessage)
pure logMsg
adminHandler _ = throwError err401
```
By contrast, the `login` handler is waiting for a `POST` with a login form.
If login is successful, it will set session cookies and return a value.
Here we're going to include lots of log messages:
```haskell
loginHandler :: CookieSettings
-> JWTSettings
-> LoginForm
-> AppM (Headers '[ Header "Set-Cookie" SetCookie, Header "Set-Cookie" SetCookie] LogMessage)
loginHandler cookieSettings jwtSettings form = do
config <- asks _getConfig
logset <- asks _getLogger
tstamp <- liftIO getCurrentTime
let logMsg = LogMessage { message = "AdminUser login attempt failed!"
, timestamp = tstamp
, level = "info"
, lversion = version config
, lenvironment = environment config
}
case validateLogin config form of
Nothing -> do
liftIO $ pushLogStrLn logset $ toLogStr logMsg
throwError err401
Just usr -> do
mApplyCookies <- liftIO $ SAS.acceptLogin cookieSettings jwtSettings usr
case mApplyCookies of
Nothing -> do
liftIO $ pushLogStrLn logset $ toLogStr logMsg
throwError err401
Just applyCookies -> do
let successMsg = logMsg{message = "AdminUser successfully authenticated!"}
liftIO $ pushLogStrLn logset $ toLogStr successMsg
pure $ applyCookies successMsg
loginHandler _ _ _ = throwError err401
validateLogin :: SiteConfig -> LoginForm -> Maybe AdminUser
validateLogin config (LoginForm uname passwd ) =
if (uname == adminUsername config) && (passwd == adminPasswd config)
then Just $ AdminUser uname
else Nothing
```
## `serveWithContext` and `hoistServerWithContext`
In order to build a working server, we'll need to `hoist` our custom monad
into Servant's Handler monad. We'll also need to pass in the proper context to ensure
authentication will work.
This will require both `serveWithContext` and `hoistServerWithContext`.
Let's define the function which will create our `Application`:
```haskell
adminLoginApi :: Proxy (AdminAndLogin '[JWT])
adminLoginApi = Proxy
mkApp :: Context '[SAS.CookieSettings, SAS.JWTSettings] -> CookieSettings -> JWTSettings -> AppCtx -> Application
mkApp cfg cs jwts ctx =
serveWithContext adminLoginApi cfg $
hoistServerWithContext adminLoginApi (Proxy :: Proxy '[SAS.CookieSettings, SAS.JWTSettings])
(flip runReaderT ctx) (adminServer cs jwts)
```
One footnote: because we'd like our logs to be in JSON form, we'll also create a `Middleware` object
so that `Warp` *also* will emit logs as JSON. This will ensure *all* logs are emitted as JSON:
```haskell
jsonRequestLogger :: IO Middleware
jsonRequestLogger =
mkRequestLogger $ def { outputFormat = CustomOutputFormatWithDetails formatAsJSON }
```
We now have all the pieces we need to serve our application inside a `main` function:
```haskell
main :: IO ()
main = do
-- typically, we'd create our config from environment variables
-- but we're going to just make one here
let config = SiteConfig "dev" "1.0.0" "admin" "secretPassword"
warpLogger <- jsonRequestLogger
appLogger <- newStdoutLoggerSet defaultBufSize
tstamp <- getCurrentTime
myKey <- generateKey
let lgmsg = LogMessage {
message = "My app starting up!"
, timestamp = tstamp
, level = "info"
, lversion = version config
, lenvironment = environment config
}
pushLogStrLn appLogger (toLogStr lgmsg) >> flushLogStr appLogger
let ctx = AppCtx config appLogger
warpSettings = Warp.defaultSettings
portSettings = Warp.setPort port warpSettings
settings = Warp.setTimeout 55 portSettings
jwtCfg = defaultJWTSettings myKey
cookieCfg = if environment config == "dev"
then defaultCookieSettings{cookieIsSecure=SAS.NotSecure}
else defaultCookieSettings
cfg = cookieCfg :. jwtCfg :. EmptyContext
Warp.runSettings settings $ warpLogger $ mkApp cfg cookieCfg jwtCfg ctx
```
## Usage
Now we can run it and try it out with `curl`. In one terminal, let's run our application
and see what our log output looks like:
```$ ./cookbook-hoist-server-with-context
{"message":"My app starting up!","timestamp":"2018-10-04T00:33:12.482568Z","level":"info","lversion":"1.0.0","lenvironment":"dev"}
```
In another terminal, let's ensure that it fails with `err401` if
we're not authenticated:
```
$ curl -v 'http://localhost:3001/admin'
< HTTP/1.1 401 Unauthorized
```
```
$ curl -v -XPOST 'http://localhost:3001/login' \
-H "Content-Type:application/json" \
-d '{"username": "bad", "password": "wrong"}'
< HTTP/1.1 401 Unauthorized
```
And in the other terminal with our log messages (from our JSON `Middleware`):
```
{"time":"03/Oct/2018:17:35:56 -0700","response":{"status":401,"size":null,"body":""},"request":{"httpVersion":"1.1","path":"/admin","size":0,"body":"","durationMs":0.22,"remoteHost":{"hostAddress":"127.0.0.1","port":51029},"headers":[["Host","localhost:3001"],["User-Agent","curl/7.60.0"],["Accept","*/*"]],"queryString":[],"method":"GET"}}
```
Now let's see that authentication works, and that we get JWTs:
```
$ curl -v -XPOST 'http://localhost:3001/login' \
-H "Content-Type:application/json" \
-d '{"username": "admin", "password": "secretPassword"}'
< HTTP/1.1 200 OK
...
< Server: Warp/3.2.25
< Content-Type: application/json;charset=utf-8
< Set-Cookie: JWT-Cookie=eyJhbGciOiJIUzUxMiJ9.eyJkYXQiOnsibmFtZSI6ImFkbWluIn19.SIoRcABKSO4mXnRifzqPWlHJUhVwuy32Qon7s1E_c3vHOsLXdXyX4V4eXOw9tMFoeIqgsXMZucqoFb36vAdKwQ; Path=/; HttpOnly; SameSite=Lax
< Set-Cookie: XSRF-TOKEN=y5PmrYHX3ywFUCwGRQqHh1TDheTLiQpwRQB3FFRd8N4=; Path=/
...
{"message":"AdminUser succesfully authenticated!","timestamp":"2018-10-04T00:37:44.455441Z","level":"info","lversion":"1.0.0","lenvironment":"dev"}
```
And in the other terminal with our log messages (note that logging out passwords is insecure...):
```
{"message":"AdminUser succesfully authenticated!","timestamp":"2018-10-04T00:37:44.455441Z","level":"info","lversion":"1.0.0","lenvironment":"dev"}
{"time":"03/Oct/2018:17:37:44 -0700","response":{"status":200,"size":null,"body":null},"request":{"httpVersion":"1.1","path":"/login","size":51,"body":"{\"username\": \"admin\", \"password\": \"secretPassword\"}","durationMs":0.23,"remoteHost":{"hostAddress":"127.0.0.1","port":51044},"headers":[["Host","localhost:3001"],["User-Agent","curl/7.60.0"],["Accept","*/*"],["Content-Type","application/json"],["Content-Length","51"]],"queryString":[],"method":"POST"}}
```
Finally, let's make sure we can access a protected resource with our tokens:
```
$ export jwt=eyJhbGciOiJIUzUxMiJ9.eyJkYXQiOnsibmFtZSI6ImFkbWluIn19.SIoRcABKSO4mXnRifzqPWlHJUhVwuy32Qon7s1E_c3vHOsLXdXyX4V4eXOw9tMFoeIqgsXMZucqoFb36vAdKwQ
$ curl -v \
-H "Authorization: Bearer $jwt" \
'http://localhost:3001/admin'
< HTTP/1.1 200 OK
{"message":"Admin User accessing admin: admin","timestamp":"2018-10-04T00:58:07.216605Z","level":"info","lversion":"1.0.0","lenvironment":"dev"}
```
And we should see this message logged-out as well:
```
{"message":"Admin User accessing admin: admin","timestamp":"2018-10-04T00:58:07.216605Z","level":"info","lversion":"1.0.0","lenvironment":"dev"}
```
This program is available as a cabal project
[here](https://github.com/haskell-servant/servant/tree/master/doc/cookbook/hoist-server-with-context).

View file

@ -1,37 +0,0 @@
cabal-version: 2.2
name: cookbook-hoist-server-with-context
version: 0.0.1
synopsis: JWT and basic access authentication with a Custom Monad cookbook example
description: Using servant-auth to support both JWT-based and basic
authentication.
homepage: http://docs.servant.dev/
license: BSD-3-Clause
license-file: ../../../servant/LICENSE
author: Servant Contributors
maintainer: haskell-servant-maintainers@googlegroups.com
category: Servant
build-type: Simple
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
executable cookbook-hoist-server-with-context
main-is: HoistServerWithContext.lhs
build-depends: base == 4.*
, base-compat
, text >= 1.2
, aeson >= 1.2
, data-default
, fast-logger
, servant
, servant-server
, servant-auth >= 0.3.2
, servant-auth-server >= 0.4.4.0
, time
, warp >= 3.2
, wai >= 3.2
, wai-extra
, http-types >= 0.12
, bytestring >= 0.10.4
, mtl
default-language: Haskell2010
ghc-options: -Wall -pgmL markdown-unlit
build-tool-depends: markdown-unlit:markdown-unlit

View file

@ -1,62 +0,0 @@
# Serving web applications over HTTPS
This short recipe shows how one can serve a servant application
over HTTPS, by simply using `warp-tls` instead of `warp` to provide
us a `run` function for running the `Application` that we get by
calling `serve`.
As usual, we start by clearing our throat of a few language extensions
and imports.
``` haskell
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
import Network.Wai
import Network.Wai.Handler.Warp
import Network.Wai.Handler.WarpTLS
import Servant
```
No need to work with a complicated API here, let's
make it as simple as it gets:
``` haskell
type API = Get '[JSON] Int
api :: Proxy API
api = Proxy
server :: Server API
server = return 10
app :: Application
app = serve api server
```
It's now time to actually run the `Application`.
The [`warp-tls`](https://hackage.haskell.org/package/warp-tls/docs/Network-Wai-Handler-WarpTLS.html)
package provides two functions for running an `Application`, called
[`runTLS`](https://hackage.haskell.org/package/warp-tls/docs/Network-Wai-Handler-WarpTLS.html#v:runTLS)
and [`runTLSSocket`](https://hackage.haskell.org/package/warp-tls/docs/Network-Wai-Handler-WarpTLS.html#v:runTLSSocket).
We will be using the first one.
It takes two arguments,
[the TLS settings](https://hackage.haskell.org/package/warp-tls/docs/Network-Wai-Handler-WarpTLS.html#t:TLSSettings)
(certificates, keys, ciphers, etc)
and [the warp settings](https://hackage.haskell.org/package/warp/docs/Network-Wai-Handler-Warp-Internal.html#t:Settings)
(port, logger, etc).
We will be using very simple settings for this example but you are of
course invited to read the documentation for those types to find out
about all the knobs that you can play with.
``` haskell
main :: IO ()
main = runTLS tlsOpts warpOpts app
where tlsOpts = tlsSettings "cert.pem" "secret-key.pem"
warpOpts = setPort 8080 defaultSettings
```
This program is available as a cabal project
[here](https://github.com/haskell-servant/servant/tree/master/doc/cookbook/https).

View file

@ -1,24 +0,0 @@
cabal-version: 2.2
name: cookbook-https
version: 0.1
synopsis: HTTPS cookbook example
homepage: http://docs.servant.dev/
license: BSD-3-Clause
license-file: ../../../servant/LICENSE
author: Servant Contributors
maintainer: haskell-servant-maintainers@googlegroups.com
build-type: Simple
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
executable cookbook-https
main-is: Https.lhs
build-depends: base == 4.*
, servant
, servant-server
, wai >= 3.2
, warp >= 3.2
, warp-tls >= 3.2.9
, markdown-unlit >= 0.4
default-language: Haskell2010
ghc-options: -Wall -pgmL markdown-unlit
build-tool-depends: markdown-unlit:markdown-unlit

View file

@ -1,40 +0,0 @@
Cookbook
========
This page is a *collective effort* whose goal is to show
how to solve many common problems with servant. If you're
interested in contributing examples of your own, feel free
to open an issue or a pull request on
`our github repository <https://github.com/haskell-servant/servant>`_
or even to just get in touch with us on the `**#haskell-servant** IRC channel
on libera.chat <https://web.libera.chat/#haskell-servant>_ or on
`the mailing list <https://groups.google.com/forum/#!forum/haskell-servant>`_.
The scope is very wide. Simple and fancy authentication schemes,
file upload, type-safe links, working with CSV, .zip archives,
you name it!
.. toctree::
:maxdepth: 1
structuring-apis/StructuringApis.lhs
generic/Generic.lhs
https/Https.lhs
db-mysql-basics/MysqlBasics.lhs
db-sqlite-simple/DBConnection.lhs
db-postgres-pool/PostgresPool.lhs
using-custom-monad/UsingCustomMonad.lhs
using-free-client/UsingFreeClient.lhs
custom-errors/CustomErrors.lhs
uverb/UVerb.lhs
basic-auth/BasicAuth.lhs
basic-streaming/Streaming.lhs
jwt-and-basic-auth/JWTAndBasicAuth.lhs
hoist-server-with-context/HoistServerWithContext.lhs
file-upload/FileUpload.lhs
pagination/Pagination.lhs
curl-mock/CurlMock.lhs
sentry/Sentry.lhs
testing/Testing.lhs
open-id-connect/OpenIdConnect.lhs
managed-resource/ManagedResource.lhs

View file

@ -1,252 +0,0 @@
# Combining JWT-based authentication with basic access authentication
In this example we will make a service with
[basic HTTP authentication](https://en.wikipedia.org/wiki/Basic_access_authentication)
for Haskell clients and other programs, as well as
with [JWT](https://en.wikipedia.org/wiki/JSON_Web_Token)-based
authentication for web browsers. Web browsers will still use basic
HTTP authentication to retrieve JWTs though.
**Warning**: this is insecure when done over plain HTTP,
so [TLS](https://en.wikipedia.org/wiki/Transport_Layer_Security)
should be used.
See [warp-tls](https://hackage.haskell.org/package/warp-tls) for that.
While basic authentication comes with Servant itself,
[servant-auth](https://hackage.haskell.org/package/servant-auth) and
[servant-auth-server](https://hackage.haskell.org/package/servant-auth-server)
packages are needed for the JWT-based one.
This recipe uses the following ingredients:
```haskell
{-# LANGUAGE OverloadedStrings, TypeFamilies, DataKinds,
DeriveGeneric, TypeOperators #-}
import Data.Aeson
import GHC.Generics
import Data.Proxy
import System.IO
import Network.HTTP.Client (newManager, defaultManagerSettings)
import Network.Wai.Handler.Warp
import Servant as S
import Servant.Client
import Servant.Auth as SA
import Servant.Auth.Server as SAS
import Control.Monad.IO.Class (liftIO)
import Data.Map as M
import Data.ByteString (ByteString)
port :: Int
port = 3001
```
## Authentication
Below is how we'll represent a user: usually user identifier is handy
to keep around, along with their role if
[role-based access control](https://en.wikipedia.org/wiki/Role-based_access_control)
is used, and other commonly needed information, such as an
organization identifier:
```haskell
data AuthenticatedUser = AUser { auID :: Int
, auOrgID :: Int
} deriving (Show, Generic)
```
The following instances are needed for JWT:
```haskell
instance ToJSON AuthenticatedUser
instance FromJSON AuthenticatedUser
instance ToJWT AuthenticatedUser
instance FromJWT AuthenticatedUser
```
We'll have to use a bit of imagination to pretend that the following
`Map` is a database connection pool:
```haskell
type Login = ByteString
type Password = ByteString
type DB = Map (Login, Password) AuthenticatedUser
type Connection = DB
type Pool a = a
initConnPool :: IO (Pool Connection)
initConnPool = pure $ fromList [ (("user", "pass"), AUser 1 1)
, (("user2", "pass2"), AUser 2 1) ]
```
See the "PostgreSQL connection pool" recipe for actual connection
pooling, and we proceed to an authentication function that would use
our improvised DB connection pool and credentials provided by a user:
```haskell
authCheck :: Pool Connection
-> BasicAuthData
-> IO (AuthResult AuthenticatedUser)
authCheck connPool (BasicAuthData login password) = pure $
maybe SAS.Indefinite Authenticated $ M.lookup (login, password) connPool
```
**Warning**: make sure to use a proper password hashing function in
functions like this: see [bcrypt](https://en.wikipedia.org/wiki/Bcrypt),
[scrypt](https://en.wikipedia.org/wiki/Scrypt),
[pgcrypto](https://www.postgresql.org/docs/current/static/pgcrypto.html).
Unlike `Servant.BasicAuth`, `Servant.Auth` uses `FromBasicAuthData`
type class for the authentication process itself. But since our
connection pool will be initialized elsewhere, we'll have to pass it
somehow: it can be done via a context entry and `BasicAuthCfg` type
family. We can actually pass a function at once, to make it a bit more
generic:
```haskell
type instance BasicAuthCfg = BasicAuthData -> IO (AuthResult AuthenticatedUser)
instance FromBasicAuthData AuthenticatedUser where
fromBasicAuthData authData authCheckFunction = authCheckFunction authData
```
## API
Test API with a couple of endpoints:
```haskell
type TestAPI = "foo" :> Capture "i" Int :> Get '[JSON] ()
:<|> "bar" :> Get '[JSON] ()
```
We'll use this for server-side functions, listing the allowed
authentication methods using the `Auth` combinator:
```haskell
type TestAPIServer =
Auth '[SA.JWT, SA.BasicAuth] AuthenticatedUser :> TestAPI
```
But `Servant.Auth.Client` only supports JWT-based authentication, so
we'll have to use regular `Servant.BasicAuth` to derive client
functions that use basic access authentication:
```haskell
type TestAPIClient = S.BasicAuth "test" AuthenticatedUser :> TestAPI
```
## Client
Client code in this setting is the same as it would be with just
`Servant.BasicAuth`, using
[servant-client](https://hackage.haskell.org/package/servant-client):
```haskell
testClient :: IO ()
testClient = do
mgr <- newManager defaultManagerSettings
let (foo :<|> _) = client (Proxy :: Proxy TestAPIClient)
(BasicAuthData "name" "pass")
res <- runClientM (foo 42)
(mkClientEnv mgr (BaseUrl Http "localhost" port ""))
hPutStrLn stderr $ case res of
Left err -> "Error: " ++ show err
Right r -> "Success: " ++ show r
```
## Server
Server code is slightly different -- we're getting `AuthResult` here:
```haskell
server :: Server TestAPIServer
server (Authenticated user) = handleFoo :<|> handleBar
where
handleFoo :: Int -> Handler ()
handleFoo n = liftIO $ hPutStrLn stderr $
concat ["foo: ", show user, " / ", show n]
handleBar :: Handler ()
handleBar = liftIO testClient
```
Catch-all for `BadPassword`, `NoSuchUser`, and `Indefinite`:
```haskell
server _ = throwAll err401
```
With `Servant.Auth`, we'll have to put both `CookieSettings` and
`JWTSettings` into context even if we're not using those, and we'll
put a partially applied `authCheck` function there as well, so that
`FromBasicAuthData` will be able to use it, while it will use our
connection pool. Otherwise it is similar to the usual way:
```haskell
mkApp :: Pool Connection -> IO Application
mkApp connPool = do
myKey <- generateKey
let jwtCfg = defaultJWTSettings myKey
authCfg = authCheck connPool
cfg = jwtCfg :. defaultCookieSettings :. authCfg :. EmptyContext
api = Proxy :: Proxy TestAPIServer
pure $ serveWithContext api cfg server
```
Finally, the main function:
```haskell
main :: IO ()
main = do
connPool <- initConnPool
let settings =
setPort port $
setBeforeMainLoop (hPutStrLn stderr
("listening on port " ++ show port)) $
defaultSettings
runSettings settings =<< mkApp connPool
```
## Usage
Now we can try it out with `curl`. First of all, let's ensure that it
fails with `err401` if we're not authenticated:
```
$ curl -v 'http://localhost:3001/bar'
< HTTP/1.1 401 Unauthorized
```
```
$ curl -v 'http://user:wrong_password@localhost:3001/bar'
< HTTP/1.1 401 Unauthorized
```
Now let's see that basic HTTP authentication works, and that we get
JWTs:
```
$ curl -v 'http://user:pass@localhost:3001/bar'
< HTTP/1.1 200 OK
< Set-Cookie: XSRF-TOKEN=lQE/sb1fW4rZ/FYUQZskI6RVRllG0CWZrQ0d3fXU4X0=; Path=/; Secure
< Set-Cookie: JWT-Cookie=eyJhbGciOiJIUzUxMiJ9.eyJkYXQiOnsiYXVPcmdJRCI6MSwiYXVJRCI6MX19.6ZQba-Co5Ul4wpmU34zXlI75wmasxDfaGRmO3BsOx-ONupX93OBfyYBCIJ3tbWMXKBVVqMDt0Pz-5CakyF2wng; Path=/; HttpOnly; Secure
```
And authenticate using JWTs alone, using the token from `JWT-Cookie`:
```
curl -v -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJkYXQiOnsiYXVPcmdJRCI6MSwiYXVJRCI6MX19.6ZQba-Co5Ul4wpmU34zXlI75wmasxDfaGRmO3BsOx-ONupX93OBfyYBCIJ3tbWMXKBVVqMDt0Pz-5CakyF2wng' 'http://localhost:3001/bar'
< HTTP/1.1 200 OK
```
This program is available as a cabal project
[here](https://github.com/haskell-servant/servant/tree/master/doc/cookbook/jwt-and-basic-auth).

View file

@ -1,36 +0,0 @@
cabal-version: 2.2
name: cookbook-jwt-and-basic-auth
version: 0.0.1
synopsis: JWT and basic access authentication cookbook example
description: Using servant-auth to support both JWT-based and basic
authentication.
homepage: http://docs.servant.dev/
license: BSD-3-Clause
license-file: ../../../servant/LICENSE
author: Servant Contributors
maintainer: haskell-servant-maintainers@googlegroups.com
category: Servant
build-type: Simple
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
executable cookbook-jwt-and-basic-auth
main-is: JWTAndBasicAuth.lhs
build-depends: base == 4.*
, text >= 1.2
, aeson >= 1.2
, containers >= 0.5
, servant
, servant-client
, servant-server
, servant-auth == 0.4.*
, servant-auth-server >= 0.3.1.0
, warp >= 3.2
, wai >= 3.2
, http-types >= 0.12
, markdown-unlit >= 0.4
, http-client >= 0.5
, bytestring >= 0.10.4
, transformers
default-language: Haskell2010
ghc-options: -Wall -pgmL markdown-unlit
build-tool-depends: markdown-unlit:markdown-unlit

View file

@ -1,114 +0,0 @@
# Request-lifetime Managed Resources
Let's see how we can write a handle that uses a resource managed by Servant. The resource is created automatically by Servant when the server recieves a request, and the resource is automatically destroyed when the server is finished handling a request.
As usual, we start with a little bit of throat clearing.
``` haskell
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
import Control.Concurrent
import Control.Exception (bracket, throwIO)
import Control.Monad.IO.Class
import Control.Monad.Trans.Resource
import Data.Acquire
import Network.HTTP.Client (newManager, defaultManagerSettings)
import Network.Wai.Handler.Warp
import Servant
import Servant.Client
import System.IO
```
Here we define an API type that uses the `WithResource` combinator. The server handler for an endpoint with a `WithResource res` component will receive a value of that type as an argument.
``` haskell
type API = WithResource Handle :> ReqBody '[PlainText] String :> Post '[JSON] NoContent
api :: Proxy API
api = Proxy
```
But this resource value has to come from somewhere. Servant obtains the value using an Acquire provided in the context. The Acquire knows how to both create and destroy resources of a particular type.
``` haskell
appContext :: Context '[Acquire Handle]
appContext = acquireHandle :. EmptyContext
acquireHandle :: Acquire Handle
acquireHandle = mkAcquire newHandle closeHandle
newHandle :: IO Handle
newHandle = do
putStrLn "opening file"
h <- openFile "test.txt" AppendMode
putStrLn "opened file"
return h
closeHandle :: Handle -> IO ()
closeHandle h = do
putStrLn "closing file"
hClose h
putStrLn "closed file"
```
Now we create the handler which will use this resource. This handler will write the request message to the System.IO.Handle which was provided to us. In some situations the handler will succeed, but in some in will fail. In either case, Servant will clean up the resource for us.
``` haskell
server :: Server API
server = writeToFile
where writeToFile :: (ReleaseKey, Handle) -> String -> Handler NoContent
writeToFile (_, h) msg = case msg of
"illegal" -> error "wait, that's illegal!"
legalMsg -> liftIO $ do
putStrLn "writing file"
hPutStrLn h legalMsg
putStrLn "wrote file"
return NoContent
```
Finally we run the server in the background while we post messages to it.
``` haskell
runApp :: IO ()
runApp = run 8080 (serveWithContext api appContext $ server)
postMsg :: String -> ClientM NoContent
postMsg = client api
main :: IO ()
main = do
mgr <- newManager defaultManagerSettings
bracket (forkIO $ runApp) killThread $ \_ -> do
ms <- flip runClientM (mkClientEnv mgr (BaseUrl Http "localhost" 8080 "")) $ do
liftIO $ putStrLn "sending hello message"
_ <- postMsg "hello"
liftIO $ putStrLn "sending illegal message"
_ <- postMsg "illegal"
liftIO $ putStrLn "done"
print ms
```
This program prints
```
sending hello message
opening file
opened file
writing file
wrote file
closing file
closed file
sending illegal message
opening file
opened file
closing file
closed file
wait, that's illegal!
CallStack (from HasCallStack):
error, called at ManagedResource.lhs:63:24 in main:Main
Left (FailureResponse (Request {requestPath = (BaseUrl {baseUrlScheme = Http, baseUrlHost = "localhost", baseUrlPort = 8080, baseUrlPath = ""},""), requestQueryString = fromList [], requestBody = Just ((),text/plain;charset=utf-8), requestAccept = fromList [], requestHeaders = fromList [], requestHttpVersion = HTTP/1.1, requestMethod = "POST"}) (Response {responseStatusCode = Status {statusCode = 500, statusMessage = "Internal Server Error"}, responseHeaders = fromList [("Transfer-Encoding","chunked"),("Date","Thu, 24 Nov 2022 21:04:47 GMT"),("Server","Warp/3.3.23"),("Content-Type","text/plain; charset=utf-8")], responseHttpVersion = HTTP/1.1, responseBody = "Something went wrong"}))
```
and appends to a file called `test.txt`. We can see from the output that when a legal message is sent, the file is opened, written to, and closed. We can also see that when an illegal message is sent, the file is opened but not written to. Crucially, it is still closed even though the handler threw an exception.

View file

@ -1,30 +0,0 @@
cabal-version: 2.2
name: cookbook-managed-resource
version: 0.1
synopsis: Simple managed resource cookbook example
homepage: http://docs.servant.dev/
license: BSD-3-Clause
license-file: ../../../servant/LICENSE
author: Servant Contributors
maintainer: haskell-servant-maintainers@googlegroups.com
build-type: Simple
tested-with: GHC==9.4.2
executable cookbook-managed-resource
main-is: ManagedResource.lhs
build-depends: base == 4.*
, text >= 1.2
, aeson >= 1.2
, servant
, servant-client
, servant-server
, warp >= 3.2
, wai >= 3.2
, http-types >= 0.12
, markdown-unlit >= 0.4
, http-client >= 0.5
, transformers
, resourcet
default-language: Haskell2010
ghc-options: -Wall -pgmL markdown-unlit
build-tool-depends: markdown-unlit:markdown-unlit

View file

@ -1,45 +0,0 @@
cabal-version: 2.2
name: open-id-connect
version: 0.1
synopsis: OpenId Connect with Servant example
homepage: http://haskell-servant.readthedocs.org/
license: BSD-3-Clause
license-file: ../../../servant/LICENSE
author: Servant Contributors
maintainer: haskell-servant-maintainers@googlegroups.com
build-type: Simple
tested-with: GHC==8.6.5
executable cookbook-openidconnect
main-is: OpenIdConnect.lhs
build-depends: base ==4.*
, aeson
, aeson-pretty
, binary
, blaze-html
, blaze-markup
, bytestring
, case-insensitive
, cereal
, containers
, generic-lens
, http-client
, http-client-tls
, http-types
, jose-jwt
, lens
, lens-aeson
, oidc-client
, protolude
, random
, servant
, servant-blaze
, servant-server
, text
, time
, vector
, wai
, warp >= 3.2
default-language: Haskell2010
ghc-options: -Wall -Wcompat -Wincomplete-uni-patterns -Wredundant-constraints -Wnoncanonical-monad-instances -pgmL markdown-unlit
build-tool-depends: markdown-unlit:markdown-unlit >= 0.4

View file

@ -1,472 +0,0 @@
[OpenID Connect](https://openid.net/connect/)
=============================================
Use OpenID Connect to authenticate your users.
This example use google OIDC provider.
It was made for a working with single page application where
some login token would be saved in the user agent local storage.
Workflow:
1. user is presented with a login button,
2. when the user clicks on the button it is redirected to the OIDC
provider,
3. the user login in the OIDC provider,
4. the OIDC provider will redirect the user and provide a `code`,
5. the server will use this code to make a POST to the OIDC provider
and will get back authentication infos,
6. The user will get display an HTML page that will save a secret
identifying him in the local storage, then it will be redirected to
/.
Let's put the imports behind us:
``` haskell
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TypeSynonymInstances #-}
module Main where
import Protolude
import Data.Aeson
(FromJSON (..), (.:))
import qualified Data.Aeson as JSON
import qualified Data.Aeson.Types as AeT
import qualified Data.ByteString.Lazy as LBS
import qualified Data.List as List
import qualified Data.Text as Text
import Jose.Jwt
(Jwt (..), decodeClaims)
import Network.HTTP.Client
(Manager, newManager)
import Network.HTTP.Client.TLS
(tlsManagerSettings)
import Network.Wai.Handler.Warp
(run)
import Servant
import Servant.HTML.Blaze
(HTML)
import qualified System.Random as Random
import Text.Blaze
(ToMarkup (..))
import qualified Text.Blaze.Html as H
import Text.Blaze.Html5
((!))
import qualified Text.Blaze.Html5 as H
import qualified Text.Blaze.Html5.Attributes as HA
import Text.Blaze.Renderer.Utf8
(renderMarkup)
import qualified Web.OIDC.Client as O
```
You'll need to create a new OpenID Connect client in an OpenID Provider.
This example was tested with Google.
You can find a list of public OIDC provider here:
https://connect2id.com/products/nimbus-oauth-openid-connect-sdk/openid-connect-providers
I copied some here:
- Google: https://developers.google.com/identity/protocols/OpenIDConnect
more precisely: https://console.developers.google.com/apis/credentials
- Microsoft: https://docs.microsoft.com/en-us/previous-versions/azure/dn645541(v=azure.100)
- Yahoo: https://developer.yahoo.com/oauth2/guide/openid_connect/
- PayPal: https://developer.paypal.com/docs/integration/direct/identity/log-in-with-paypal/
During the configuration you'll need to provide a redirect uri.
The redirect_uri should correspond to the uri user will be redirected to
after a successful login into the OpenID provider.
So during your test, you should certainly just use `http://localhost:3000/login/cb`.
In general you should use your own domain name.
You'll then be given a `client_id` and a `client_password`.
Fill those values in here:
``` haskell
oidcConf :: OIDCConf
oidcConf = OIDCConf { redirectUri = "http://localhost:3000/login/cb"
, clientId = "xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com"
, clientPassword = "************************" }
```
Then we declare our main server:
``` haskell
main :: IO ()
main = do
oidcEnv <- initOIDC oidcConf
run 3000 (app oidcEnv)
type API = IdentityRoutes Customer
:<|> Get '[HTML] Homepage
api :: Proxy API
api = Proxy
server :: OIDCEnv -> Server API
server oidcEnv = serveOIDC oidcEnv handleOIDCLogin
:<|> return Homepage
-- | Then main app
app :: OIDCEnv -> Application
app oidcEnv = serve api (server oidcEnv)
```
OIDC
----
That part try to separate concern, and certainly in a real world
application that should be in its distinct module.
``` haskell
-- * OIDC
data OIDCConf =
OIDCConf { redirectUri :: ByteString
, clientId :: ByteString
, clientPassword :: ByteString
} deriving (Show, Eq)
```
First we need to initialize OIDC.
A short explanation about it:
- to complete the workflow we need to make a POST request to the OIDC provider.
So we need to create an http manager to make those call properly.
- Then in order to prevent replay attack, each time an user wants to login we
should provide a random string called the `state`. When the user is
redirected to the `redirect_uri`, the OIDC provider should provide the same
`state` along a `code` parameter.
``` haskell
initOIDC :: OIDCConf -> IO OIDCEnv
initOIDC OIDCConf{..} = do
mgr <- newManager tlsManagerSettings
prov <- O.discover "https://accounts.google.com" mgr
let oidc = O.setCredentials clientId clientPassword redirectUri (O.newOIDC prov)
return OIDCEnv { oidc = oidc
, mgr = mgr
, genState = genRandomBS
, prov = prov
, redirectUri = redirectUri
, clientId = clientId
, clientPassword = clientPassword
}
data OIDCEnv = OIDCEnv { oidc :: O.OIDC
, mgr :: Manager
, genState :: IO ByteString
, prov :: O.Provider
, redirectUri :: ByteString
, clientId :: ByteString
, clientPassword :: ByteString
}
```
The `IdentityRoutes` are two endpoints:
- an endpoint to redirect the users to the OIDC Provider,
- another one the user will be redirected to from the OIDC Provider.
``` haskell
type IdentityRoutes a =
"login" :> ( -- redirect User to the OpenID Provider
Get '[JSON] NoContent
-- render the page that will save the user creds in the user-agent
:<|> "cb" :> QueryParam "error" Text
:> QueryParam "code" Text
:> Get '[HTML] User)
-- | gen a 302 redirect helper
redirects :: (StringConv s ByteString) => s -> Handler ()
redirects url = throwError err302 { errHeaders = [("Location",toS url)]}
```
That function will generate the URL to redirect the users to when
they'll click on the login link: `https://yourdomain/login`.
``` haskell
genOIDCURL :: OIDCEnv -> IO ByteString
genOIDCURL OIDCEnv{..} = do
st <- genState -- generate a random string
let oidcCreds = O.setCredentials clientId clientPassword redirectUri (O.newOIDC prov)
loc <- O.getAuthenticationRequestUrl oidcCreds [O.openId, O.email, O.profile] (Just st) []
return (show loc)
handleLogin :: OIDCEnv -> Handler NoContent
handleLogin oidcenv = do
loc <- liftIO (genOIDCURL oidcenv)
redirects loc
return NoContent
```
The `AuthInfo` is about the infos we can grab from OIDC provider.
To be more precise, the user should come with a `code` (a token) and
POSTing that code to the correct OIDC provider endpoint should return a JSON
object. One of the fields should be named `id_token` which should be a
JWT containing all the information we need. Depending on the scopes we
asked we might get more information.
``` haskell
-- | @AuthInfo@
data AuthInfo = AuthInfo { email :: Text
, emailVerified :: Bool
, name :: Text } deriving (Eq, Show, Generic)
instance FromJSON AuthInfo where
parseJSON (JSON.Object v) = do
email :: Text <- v .: "email"
email_verified :: Bool <- v .: "email_verified"
name :: Text <- v .: "name"
return $ AuthInfo (toS email) email_verified (toS name)
parseJSON invalid = AeT.typeMismatch "Coord" invalid
instance JSON.ToJSON AuthInfo where
toJSON (AuthInfo e ev n) =
JSON.object [ "email" JSON..= (toS e :: Text)
, "email_verified" JSON..= ev
, "name" JSON..= (toS n :: Text)
]
type LoginHandler = AuthInfo -> IO (Either Text User)
```
The `handleLoggedIn` is that part that will retrieve the information from
the user once he is redirected from the OIDC Provider after login.
If the user is redirected to the `redirect_uri` but with an `error` query
parameter then it means something went wrong.
If there is no error query param but a `code` query param it means the user
successfully logged in. From there we need to make a request to the token
endpoint of the OIDC provider. It's a POST that should contain the code
as well as the client id and secret.
Making this HTTP POST is the responsibility of `requestTokens`.
From there we extract the `claims` of the JWT contained in one of the value
of the JSON returned by the POST HTTP Request.
``` haskell
data User = User { userId :: Text
, userSecret :: Text
, localStorageKey :: Text
, redirectUrl :: Maybe Text
} deriving (Show,Eq,Ord)
handleLoggedIn :: OIDCEnv
-> LoginHandler -- ^ handle successful id
-> Maybe Text -- ^ error
-> Maybe Text -- ^ code
-> Handler User
handleLoggedIn oidcenv handleSuccessfulId err mcode =
case err of
Just errorMsg -> forbidden errorMsg
Nothing -> case mcode of
Just oauthCode -> do
tokens <- liftIO $ O.requestTokens (oidc oidcenv) (toS oauthCode) (mgr oidcenv)
putText . show . O.claims . O.idToken $ tokens
let jwt = toS . unJwt . O.jwt . O.idToken $ tokens
eAuthInfo = decodeClaims jwt :: Either O.JwtError (O.JwtHeader,AuthInfo)
case eAuthInfo of
Left jwtErr -> forbidden $ "JWT decode/check problem: " <> show jwtErr
Right (_,authInfo) ->
if emailVerified authInfo
then do
user <- liftIO $ handleSuccessfulId authInfo
either forbidden return user
else forbidden "Please verify your email"
Nothing -> do
liftIO $ putText "No code param"
forbidden "no code parameter given"
```
When you render a User with blaze-html, it will generate a page with a js
that will put a secret for that user in the local storage. And it will
redirect the user to /.
``` haskell
instance ToMarkup User where
toMarkup User{..} = H.docTypeHtml $ do
H.head $
H.title "Logged In"
H.body $ do
H.h1 "Logged In"
H.p (H.toHtml ("Successful login with id " <> userId))
H.script (H.toHtml ("localStorage.setItem('" <> localStorageKey <> "','" <> userSecret <> "');"
<> "localStorage.setItem('user-id','" <> userId <> "');"
<> "window.location='" <> fromMaybe "/" redirectUrl <> "';" -- redirect the user to /
));
serveOIDC :: OIDCEnv -> LoginHandler -> Server (IdentityRoutes a)
serveOIDC oidcenv loginHandler =
handleLogin oidcenv :<|> handleLoggedIn oidcenv loginHandler
-- * Auth
type APIKey = ByteString
type Account = Text.Text
type Conf = [(APIKey,Account)]
data Customer = Customer {
account :: Account
, apiKey :: APIKey
, mail :: Maybe Text
, fullname :: Maybe Text
}
```
Here is the code that displays the homepage.
It should contain a link to the `/login` URL.
When the user clicks on this link it will be redirected to Google login page
with some generated information.
The page also displays the content of the local storage.
And in particular the items `api-key` and `user-id`.
Those items should be set after a successful login when the user is redirected to
`/login/cb`.
The logic used generally is to use that api-key to uniquely identify an user.
Another option would have been to set a cookie.
``` haskell
data Homepage = Homepage
instance ToMarkup Homepage where
toMarkup Homepage = H.docTypeHtml $ do
H.head $ do
H.title "OpenID Connect Servant Example"
H.style (H.toHtml ("body { font-family: monospace; font-size: 18px; }" :: Text.Text))
H.body $ do
H.h1 "OpenID Connect Servant Example"
H.div $
H.a ! HA.href "/login" $ "Click here to login"
H.ul $ do
H.li $ do
H.span "API Key in Local storage: "
H.script (H.toHtml ("document.write(localStorage.getItem('api-key'));" :: Text.Text))
H.li $ do
H.span "User ID in Local storage: "
H.script (H.toHtml ("document.write(localStorage.getItem('user-id'));" :: Text.Text))
```
We need some helpers to generate random string for generating state and API Keys.
``` haskell
-- | generate a random ByteString, not necessarily extremely good randomness
-- still the password will be long enough to be very difficult to crack
genRandomBS :: IO ByteString
genRandomBS = do
g <- Random.newStdGen
Random.randomRs (0, n) g & take 42 & fmap toChar & readable 0 & toS & return
where
n = length letters - 1
toChar i = letters List.!! i
letters = ['A'..'Z'] <> ['0'..'9'] <> ['a'..'z']
readable :: Int -> [Char] -> [Char]
readable _ [] = []
readable i str =
let blocksize = case n of
0 -> 8
1 -> 4
2 -> 4
3 -> 4
_ -> 12
block = take blocksize str
rest = drop blocksize str
in if List.null rest
then str
else block <> "-" <> readable (i+1) rest
customerFromAuthInfo :: AuthInfo -> IO Customer
customerFromAuthInfo authinfo = do
apikey <- genRandomBS
return Customer { account = toS (email authinfo)
, apiKey = apikey
, mail = Just (toS (email authinfo))
, fullname = Just (toS (name authinfo))
}
handleOIDCLogin :: LoginHandler
handleOIDCLogin authInfo = do
custInfo <- customerFromAuthInfo authInfo
if emailVerified authInfo
then return . Right . customerToUser $ custInfo
else return (Left "You emails is not verified by your provider. Please verify your email.")
where
customerToUser :: Customer -> User
customerToUser c =
User { userId = toS (account c)
, userSecret = toS (apiKey c)
, redirectUrl = Nothing
, localStorageKey = "api-key"
}
```
`Error` helpers
---------------
``` haskell
data Err = Err { errTitle :: Text
, errMsg :: Text }
instance ToMarkup Err where
toMarkup Err{..} = H.docTypeHtml $ do
H.head $ do
H.title "Error"
H.body $ do
H.h1 (H.a ! HA.href "/" $ "Home")
H.h2 (H.toHtml errTitle)
H.p (H.toHtml errMsg)
format :: ToMarkup a => a -> LBS.ByteString
format err = toMarkup err & renderMarkup
appToErr :: ServerError -> Text -> ServerError
appToErr x msg = x
{ errBody = toS $ format (Err (toS (errReasonPhrase x)) msg)
, errHeaders = [("Content-Type","text/html")]}
unauthorized :: (MonadError ServerError m) => Text -> m a
unauthorized = throwError . unauthorizedErr
unauthorizedErr :: Text -> ServerError
unauthorizedErr = appToErr err401
forbidden :: (MonadError ServerError m) => Text -> m a
forbidden = throwError . forbiddenErr
forbiddenErr :: Text -> ServerError
forbiddenErr = appToErr err403
notFound :: ( MonadError ServerError m) => Text -> m a
notFound = throwError . notFoundErr
notFoundErr :: Text -> ServerError
notFoundErr = appToErr err404
preconditionFailed :: ( MonadError ServerError m) => Text -> m a
preconditionFailed = throwError . preconditionFailedErr
preconditionFailedErr :: Text -> ServerError
preconditionFailedErr = appToErr err412
serverError :: ( MonadError ServerError m) => Text -> m a
serverError = throwError . serverErrorErr
serverErrorErr :: Text -> ServerError
serverErrorErr = appToErr err500
```

View file

@ -1,250 +0,0 @@
# Pagination
## Overview
Let's see an approach to typed pagination with *Servant* using [servant-pagination](https://hackage.haskell.org/package/servant-pagination).
This module offers opinionated helpers to declare a type-safe and a flexible pagination
mechanism for Servant APIs. This design, inspired by [Heroku's API](https://devcenter.heroku.com/articles/platform-api-reference#ranges),
provides a small framework to communicate about a possible pagination feature of an endpoint,
enabling a client to consume the API in different fashions (pagination with offset / limit,
endless scroll using last referenced resources, ascending and descending ordering, etc.)
Therefore, client can provide a `Range` header with their request with the following format:
- `Range: <field> [<value>][; offset <o>][; limit <l>][; order <asc|desc>]`
For example: `Range: createdAt 2017-01-15T23:14:67.000Z; offset 5; order desc` indicates that
the client is willing to retrieve the next batch of document in descending order that were
created after the fifteenth of January, skipping the first 5.
As a response, the server may return the list of corresponding documents, and augment the
response with 3 headers:
- `Accept-Ranges`: A comma-separated list of fields upon which a range can be defined
- `Content-Range`: Actual range corresponding to the content being returned
- `Next-Range`: Indicate what should be the next `Range` header in order to retrieve the next range
For example:
- `Accept-Ranges: createdAt, modifiedAt`
- `Content-Range: createdAt 2017-01-15T23:14:51.000Z..2017-02-18T06:10:23.000Z`
- `Next-Range: createdAt 2017-02-19T12:56:28.000Z; offset 0; limit 100; order desc`
## Getting Started
Code-wise the integration is quite seamless and unobtrusive. `servant-pagination` provides a
`Ranges (fields :: [Symbol]) (resource :: *) -> *` data-type for declaring available ranges
on a group of _fields_ and a target _resource_. To each combination (resource + field) is
associated a given type `RangeType (resource :: *) (field :: Symbol) -> *` as described by
the type-family in the `HasPagination` type-class.
So, let's start with some imports and extensions to get this out of the way:
``` haskell
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
import Data.Aeson
(ToJSON, genericToJSON)
import Data.Maybe
(fromMaybe)
import Data.Proxy
(Proxy (..))
import GHC.Generics
(Generic)
import Servant
((:>), GetPartialContent, Handler, Header, Headers, JSON, Server, addHeader)
import Servant.Pagination
(HasPagination (..), PageHeaders, Range (..), Ranges, RangeOptions(..),
applyRange, extractRange, returnRange)
import qualified Data.Aeson as Aeson
import qualified Network.Wai.Handler.Warp as Warp
import qualified Servant
import qualified Servant.Pagination as Pagination
```
#### Declaring the Resource
Servant APIs are rather resource-oriented, and so is `servant-pagination`. This
guide shows a basic example working with `JSON` (as you could tell from the
import list already). To make the world a <span style='text-decoration:
line-through'>better</span> colored place, let's create an API to retrieve
colors -- with pagination.
``` haskell
data Color = Color
{ name :: String
, rgb :: [Int]
, hex :: String
} deriving (Eq, Show, Generic)
instance ToJSON Color where
toJSON =
genericToJSON Aeson.defaultOptions
colors :: [Color]
colors =
[ Color "Black" [0, 0, 0] "#000000"
, Color "Blue" [0, 0, 255] "#0000ff"
, Color "Green" [0, 128, 0] "#008000"
, Color "Grey" [128, 128, 128] "#808080"
, Color "Purple" [128, 0, 128] "#800080"
, Color "Red" [255, 0, 0] "#ff0000"
, Color "Yellow" [255, 255, 0] "#ffff00"
]
```
#### Declaring the Ranges
Now that we have defined our _resource_ (a.k.a `Color`), we are ready to declare a new `Range`
that will operate on a "name" field (genuinely named after the `name` fields from the `Color`
record).
For that, we need to tell `servant-pagination` two things:
- What is the type of the corresponding `Range` values
- How do we get one of these values from our resource
This is done via defining an instance of `HasPagination` as follows:
``` haskell
instance HasPagination Color "name" where
type RangeType Color "name" = String
getFieldValue _ = name
-- getRangeOptions :: Proxy "name" -> Proxy Color -> RangeOptions
-- getDefaultRange :: Proxy Color -> Range "name" String
defaultRange :: Range "name" String
defaultRange =
getDefaultRange (Proxy @Color)
```
Note that `getFieldValue :: Proxy "name" -> Color -> String` is the minimal complete definition
of the class. Yet, you can define `getRangeOptions` to provide different parsing options (see
the last section of this guide). In the meantime, we've also defined a `defaultRange` as it will
come in handy when defining our handler.
#### API
Good, we have a resource, we have a `Range` working on that resource, we can now declare our
API using other Servant combinators we already know:
``` haskell
type API =
"colors"
:> Header "Range" (Ranges '["name"] Color)
:> GetPartialContent '[JSON] (Headers MyHeaders [Color])
type MyHeaders =
Header "Total-Count" Int ': PageHeaders '["name"] Color
```
`PageHeaders` is a type alias provided by the library to declare the necessary response headers
we mentioned in introduction. Expanding the alias boils down to the following:
``` haskell
-- type MyHeaders =
-- '[ Header "Total-Count" Int
-- , Header "Accept-Ranges" (AcceptRanges '["name"])
-- , Header "Content-Range" (ContentRange '["name"] Color)
-- , Header "Next-Range" (Ranges '["name"] Color)
-- ]
```
As a result, we will need to provide all those headers with the response in our handler. Worry
not, _servant-pagination_ provides an easy way to lift a collection of resources into such handler.
#### Server
Time to connect the last bits by defining the server implementation of our colorful API. The `Ranges`
type we've defined above (tied to the `Range` HTTP header) indicates the server to parse any `Range`
header, looking for the format defined in introduction with fields and target types we have just declared.
If no such header is provided, we will end up receiving `Nothing`. Otherwise, it will be possible
to _extract_ a `Range` from our `Ranges`.
``` haskell
server :: Server API
server = handler
where
handler :: Maybe (Ranges '["name"] Color) -> Handler (Headers MyHeaders [Color])
handler mrange = do
let range =
fromMaybe defaultRange (mrange >>= extractRange)
addHeader (length colors) <$> returnRange range (applyRange range colors)
main :: IO ()
main =
Warp.run 1442 $ Servant.serve (Proxy @API) server
```
Let's try it out using different ranges to observe the server's behavior. As a reminder, here's
the format we defined, where `<field>` here can only be `name` and `<value>` must parse to a `String`:
- `Range: <field> [<value>][; offset <o>][; limit <l>][; order <asc|desc>]`
Beside the target field, everything is pretty much optional in the `Range` HTTP header. Missing parts
are deduced from the `RangeOptions` that are part of the `HasPagination` instance. Therefore, all
following examples are valid requests to send to our server:
- 1 - `curl http://localhost:1442/colors -vH 'Range: name'`
- 2 - `curl http://localhost:1442/colors -vH 'Range: name; limit 2'`
- 3 - `curl http://localhost:1442/colors -vH 'Range: name Green; order asc; offset 1'`
Considering the following default options:
- `defaultRangeLimit: 100`
- `defaultRangeOffset: 0`
- `defaultRangeOrder: RangeDesc`
The previous ranges reads as follows:
- 1 - The first 100 colors, ordered by descending names
- 2 - The first 2 colors, ordered by descending names
- 3 - The 100 colors after `Green` (not included), ordered by ascending names.
## Going Forward
#### Multiple Ranges
Note that in the simple above scenario, there's no ambiguity with `extractRange` and `returnRange`
because there's only one possible `Range` defined on our resource. Yet, as you've most probably
noticed, the `Ranges` combinator accepts a list of fields, each of which must declare a `HasPagination`
instance. Doing so will make the other helper functions more ambiguous and type annotations are
highly likely to be needed.
``` haskell
instance HasPagination Color "hex" where
type RangeType Color "hex" = String
getFieldValue _ = hex
-- to then define: Ranges '["name", "hex"] Color
```
#### Parsing Options
By default, `servant-pagination` provides an implementation of `getRangeOptions` for each
`HasPagination` instance. However, this can be overridden when defining the instance to provide
your own options. These options come into play when a `Range` header is received and isn't fully
specified (`limit`, `offset`, `order` are all optional) to provide default fallback values for those.
For instance, let's say we wanted to change the default limit to `5` in a new range on
`"rgb"`, we could tweak the corresponding `HasPagination` instance as follows:
``` haskell
instance HasPagination Color "rgb" where
type RangeType Color "rgb" = Int
getFieldValue _ = sum . rgb
getRangeOptions _ _ = Pagination.defaultOptions { defaultRangeLimit = 5 }
```

View file

@ -1,23 +0,0 @@
cabal-version: 2.2
name: cookbook-pagination
version: 2.1
synopsis: Pagination with Servant example
homepage: http://docs.servant.dev/
license: BSD-3-Clause
license-file: ../../../servant/LICENSE
author: Servant Contributors
maintainer: haskell-servant-maintainers@googlegroups.com
build-type: Simple
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
executable cookbook-pagination
main-is: Pagination.lhs
build-tool-depends: markdown-unlit:markdown-unlit
default-language: Haskell2010
ghc-options: -Wall -pgmL markdown-unlit
build-depends: base >= 4.9 && <5
, aeson
, servant
, servant-server
, servant-pagination >= 2.1.0 && < 3.0.0
, warp

View file

@ -1,116 +0,0 @@
# Error logging with Sentry
In this recipe we will use [Sentry](https://sentry.io) to collect the runtime exceptions generated by our application. We will use the [raven-haskell](https://hackage.haskell.org/package/raven-haskell) package, which is a client for a Sentry event server. Mind that this package is not present on [Stackage](https://www.stackage.org/), so if we are using [Stack](https://docs.haskellstack.org) well need to add it to our `extra-deps` section in the `stack.yaml` file.
To exemplify this we will need the following imports
```haskell
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
import Control.Exception (Exception,
SomeException, throw)
import Data.ByteString.Char8 (unpack)
import Network.Wai (Request, rawPathInfo,
requestHeaderHost)
import Network.Wai.Handler.Warp (defaultOnException,
defaultSettings,
runSettings,
setOnException,
setPort)
import Servant
import System.Log.Raven (initRaven, register,
silentFallback)
import System.Log.Raven.Transport.HttpConduit (sendRecord)
import System.Log.Raven.Types (SentryLevel (Error),
SentryRecord (..))
```
Just for the sake of the example we will use the following API which will throw an exception
```haskell
type API = "break" :> Get '[JSON] ()
data MyException = MyException deriving (Show)
instance Exception MyException
server = breakHandler
where breakHandler :: Handler ()
breakHandler = do
throw MyException
return ()
```
First thing we need to do if we want to intercept and log this exception, we need to look in the section of our code where we run the `warp` application, and instead of using the simple `run` function from `warp`, we use the `runSettings` functions which allows to customise the handling of requests
```haskell
main :: IO ()
main =
let
settings =
setPort 8080 $
setOnException sentryOnException $
defaultSettings
in
runSettings settings $ serve (Proxy :: Proxy API) server
```
The definition of the `sentryOnException` function could look as follows
```haskell
sentryOnException :: Maybe Request -> SomeException -> IO ()
sentryOnException mRequest exception = do
sentryService <- initRaven
"https://username:password@senty.host/id"
id
sendRecord
silentFallback
register
sentryService
"myLogger"
Error
(formatMessage mRequest exception)
(recordUpdate mRequest exception)
defaultOnException mRequest exception
```
It does three things. First it initializes the service which will communicate with Sentry. The parameters it receives are:
- the Sentry `DSN`, which is obtained when creating a new project on Sentry
- a default way to update sentry fields, where we use the identity function
- an event transport, which generally would be `sendRecord`, an HTTPS capable transport which uses http-conduit
- a fallback handler, which we choose to be `silentFallback` since later we are logging to the console anyway.
In the second step it actually sends our message to Sentry with the `register` function. Its arguments are:
- the configured Sentry service which we just created
- the name of the logger
- the error level (see [SentryLevel](https://hackage.haskell.org/package/raven-haskell/docs/System-Log-Raven-Types.html#t:SentryLevel) for the possible options)
- the message we want to send
- an update function to handle the specific `SentryRecord`
Eventually it just delegates the error handling to the default warp mechanism.
The function `formatMessage` simply uses the request and the exception to return a string with the error message.
```haskell
formatMessage :: Maybe Request -> SomeException -> String
formatMessage Nothing exception = "Exception before request could be parsed: " ++ show exception
formatMessage (Just request) exception = "Exception " ++ show exception ++ " while handling request " ++ show request
```
The only piece left now is the `recordUpdate` function which allows to decorate with other [attributes](https://docs.sentry.io/clientdev/attributes/) the default `SentryRecord`.
```haskell
recordUpdate :: Maybe Request -> SomeException -> SentryRecord -> SentryRecord
recordUpdate Nothing exception record = record
recordUpdate (Just request) exception record = record
{ srCulprit = Just $ unpack $ rawPathInfo request
, srServerName = fmap unpack $ requestHeaderHost request
}
```
In this examples we set the raw path as the culprit and we use the `Host` header to populate the server name field.
You can try to run this code using the `cookbook-sentry` executable. You should obtain a `MyException` error in the console and, if you provided a valid Sentry DSN, you should also find your error in the Sentry interface.

View file

@ -1,24 +0,0 @@
cabal-version: 2.2
name: cookbook-sentry
version: 0.1
synopsis: Collecting runtime exceptions using Sentry
homepage: http://docs.servant.dev/
license: BSD-3-Clause
license-file: ../../../servant/LICENSE
author: Servant Contributors
maintainer: haskell-servant-maintainers@googlegroups.com
build-type: Simple
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
executable cookbook-sentry
main-is: Sentry.lhs
build-depends: base == 4.*
, bytestring
, markdown-unlit >= 0.4
, raven-haskell >= 0.1.2
, servant-server
, warp >= 3.2
, wai >= 3.2
default-language: Haskell2010
ghc-options: -Wall -pgmL markdown-unlit
build-tool-depends: markdown-unlit:markdown-unlit

View file

@ -1,205 +0,0 @@
# Structuring APIs
In this recipe, we will see a few simple ways to
structure your APIs by splitting them up into smaller
"sub-APIs" or by sharing common structure between
different parts. Let's start with the usual throat
clearing.
``` haskell
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE KindSignatures #-}
import Data.Aeson
import GHC.Generics
import GHC.TypeLits
import Network.Wai.Handler.Warp
import Servant
```
Our application will consist of three different
"sub-APIs", with a few endpoints in each of them.
Our global API is defined as follows.
``` haskell
type API = FactoringAPI
:<|> SimpleAPI "users" User UserId
:<|> SimpleAPI "products" Product ProductId
```
We simply join the three different parts with `:<|>`,
as if each sub-API was just a simple endpoint.
The first part, `FactoringAPI`, shows how we can
"factor out" combinators that are common to several
endpoints, just like we turn `a * b + a * c` into
`a * (b + c)` in algebra.
``` haskell
-- Two endpoints:
-- - GET /x/<some 'Int'>[?y=<some 'Int'>]
-- - POST /x/<some 'Int'>
type FactoringAPI =
"x" :> Capture "x" Int :>
( QueryParam "y" Int :> Get '[JSON] Int
:<|> Post '[JSON] Int
)
{- this is equivalent to:
type FactoringAPI' =
"x" :> Capture "x" Int :> QueryParam "y" Int :> Get '[JSON] Int :<|>
"x" :> Capture "x" Int :> Post '[JSON] Int
-}
```
You can see that both endpoints start with a static
path fragment, `/"x"`, then capture some arbitrary
`Int` until they finally differ. Now, this also has
an effect on the server for such an API, and its type
in particular. While the server for `FactoringAPI'` would
be made of a function of type `Int -> Maybe Int -> Handler Int`
and a function of type `Int -> Handler Int` glued with `:<|>`,
a server for `FactoringAPI` (without the `'`) reflects the
"factorisation" and therefore, `Server FactoringAPI` is
`Int -> (Maybe Int -> Handler Int :<|> Handler Int)`. That is, the
server must be a function that takes an `Int` (the `Capture`) and
returns two values glued with `:<|>`, one of type `Maybe Int -> Handler Int`
and the other of type `Handler Int`. Let's provide such a server
implementation, with those "nested types".
**Tip**: you can load this module in ghci and ask for the concrete
type that `Server FactoringAPI` "resolves to" by typing
`:kind! Server FactoringAPI`.
``` haskell
factoringServer :: Server FactoringAPI
factoringServer x = getXY :<|> postX
where getXY Nothing = return x
getXY (Just y) = return (x + y)
postX = return (x - 1)
```
If you want to avoid the "nested types" and the need to manually
dispatch the arguments (like `x` above) to the different request
handlers, and would just like to be able to declare the API type
as above but pretending that the `Capture` is not factored out,
that every combinator is "distributed" (i.e that all endpoints
are specified like `FactoringAPI'` above), then you should
look at `flatten` from the
[servant-flatten](https://hackage.haskell.org/package/servant-flatten)
package.
Next come the two sub-APIs defined in terms of this `SimpleAPI`
type, but with different parameters. That type is just a good old
Haskell type synonym that abstracts away a pretty common structure in
web services, where you have:
- one endpoint for listing a bunch of entities of some type
- one endpoint for accessing the entity with a given identifier
- one endpoint for creating a new entity
There are many variants on this theme (endpoints for deleting,
paginated listings, etc). The simple definition below reproduces
such a structure, but instead of picking concrete types for
the entities and their identifiers, we simply let the user
of the type decide, by making those types parameters of
`SimpleAPI`. While we're at it, we'll put all our endpoints
under a common prefix that we also take as a parameter.
``` haskell
-- Three endpoints:
-- - GET /<name>
-- - GET /<name>/<some 'i'>
-- - POST /<name>
type SimpleAPI (name :: Symbol) a i = name :>
( Get '[JSON] [a]
:<|> Capture "id" i :> Get '[JSON] a
:<|> ReqBody '[JSON] a :> Post '[JSON] NoContent
)
```
`Symbol` is the [kind](https://wiki.haskell.org/Kind)
of type-level strings, which is what servant uses for
representing static path fragments. We can even provide
a little helper function for creating a server for that API
given one handler for each endpoint as arguments.
``` haskell
simpleServer
:: Handler [a]
-> (i -> Handler a)
-> (a -> Handler NoContent)
-> Server (SimpleAPI name a i)
simpleServer listAs getA postA =
listAs :<|> getA :<|> postA
{- you could alternatively provide such a definition
but with the handlers running in another monad,
or even an arbitrary one!
simpleAPIServer
:: m [a]
-> (i -> m a)
-> (a -> m NoContent)
-> ServerT (SimpleAPI name a i) m
simpleAPIServer listAs getA postA =
listAs :<|> getA :<|> postA
and use 'hoistServer' on the result of `simpleAPIServer`
applied to your handlers right before you call `serve`.
-}
```
We can use this to define servers for the user and product
related sections of the API.
``` haskell
userServer :: Server (SimpleAPI "users" User UserId)
userServer = simpleServer
(return [])
(\userid -> return $
if userid == 0
then User "john" 64
else User "everybody else" 10
)
(\_user -> return NoContent)
productServer :: Server (SimpleAPI "products" Product ProductId)
productServer = simpleServer
(return [])
(\_productid -> return $ Product "Great stuff")
(\_product -> return NoContent)
```
Finally, some dummy types and the serving part.
``` haskell
type UserId = Int
data User = User { username :: String, age :: Int }
deriving Generic
instance FromJSON User
instance ToJSON User
type ProductId = Int
data Product = Product { productname :: String }
deriving Generic
instance FromJSON Product
instance ToJSON Product
api :: Proxy API
api = Proxy
main :: IO ()
main = run 8080 . serve api $
factoringServer :<|> userServer :<|> productServer
```
This program is available as a cabal project
[here](https://github.com/haskell-servant/servant/tree/master/doc/cookbook/structuring-apis).

View file

@ -1,23 +0,0 @@
cabal-version: 2.2
name: cookbook-structuring-apis
version: 0.1
synopsis: Example that shows how APIs can be structured
homepage: http://docs.servant.dev/
license: BSD-3-Clause
license-file: ../../../servant/LICENSE
author: Servant Contributors
maintainer: haskell-servant-maintainers@googlegroups.com
build-type: Simple
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
executable cookbook-structuring-apis
main-is: StructuringApis.lhs
build-depends: base == 4.*
, aeson >= 1.2
, servant
, servant-server
, warp >= 3.2
, markdown-unlit >= 0.4
default-language: Haskell2010
ghc-options: -Wall -pgmL markdown-unlit
build-tool-depends: markdown-unlit:markdown-unlit

View file

@ -1,506 +0,0 @@
# How To Test Servant Applications
Even with a nicely structured API that passes Haskell's strict type checker,
it's a good idea to write some tests for your application.
In this recipe we'll work through some common testing strategies and provide
examples of utlizing these testing strategies in order to test Servant
applications.
## Testing strategies
There are many testing strategies you may wish to employ when testing your
Servant application, but included below are three common testing patterns:
- We'll use `servant-client` to derive client functions and then send valid
requests to our API, running in another thread. This is great for testing
that our **business logic** is correctly implemented with only valid HTTP
requests.
- We'll also use `hspec-wai` to make **arbitrary HTTP requests**, in order to
test how our application may respond to invalid or otherwise unexpected
requests.
- Finally, we can also use `servant-quickcheck` for **whole-API tests**, in order
to assert that our entire application conforms to **best practices**.
## Useful Libraries
The following libraries will often come in handy when we decide to test our
Servant applications:
- [hspec](https://hspec.github.io/)
- [hspec-wai](http://hackage.haskell.org/package/hspec-wai)
- [QuickCheck](http://hackage.haskell.org/package/QuickCheck)
- [servant-quickcheck](https://hackage.haskell.org/package/servant-quickcheck)
## Imports and Our Testing Module
This recipe starts with the following ingredients:
```haskell
{-# LANGUAGE OverloadedStrings, TypeFamilies, DataKinds,
DeriveGeneric, TypeOperators #-}
import Prelude ()
import Prelude.Compat
import qualified Control.Concurrent as C
import Control.Concurrent.MVar
import Control.Exception (bracket)
import Control.Lens hiding (Context)
import Data.Aeson
import Data.Aeson.Lens
import qualified Data.HashMap.Strict as HM
import Data.Text (Text, unpack)
import GHC.Generics
import Network.HTTP.Client hiding (Proxy)
import Network.HTTP.Types
import Network.Wai
import qualified Network.Wai.Handler.Warp as Warp
import Servant
import Servant.Client
import Servant.Server
import Servant.QuickCheck
import Servant.QuickCheck.Internal (serverDoesntSatisfy)
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.Matcher
```
We're going to produce different `Spec`s that represent different
aspects of our application, and we'll ask `hspec` to run all of our different
`Spec`s. This is a common organizational method for testing modules:
```haskell
spec :: Spec
spec = do
businessLogicSpec
thirdPartyResourcesSpec
servantQuickcheckSpec
```
Often, codebases will use `hspec`'s
[autodiscover pragma](http://hspec.github.io/hspec-discover.html)
to find all testing modules and `Spec`s inside, but we're going to
explicitly make a `main` function to run our tests because we have only one
`spec` defined above:
```haskell
main :: IO ()
main = hspec spec
```
## Testing Your Business Logic
Let's say we have an API that looks something like this:
```haskell
data User = User {
name :: Text
, user_id :: Integer
} deriving (Eq, Show, Generic)
instance FromJSON User
instance ToJSON User
type UserApi =
-- One endpoint: create a user
"user" :> Capture "userId" Integer :> Post '[JSON] User
```
A real server would likely use a database to store, retrieve, and validate
users, but we're going to do something really simple merely to have something
to test. With that said, here's a sample handler, server, and `Application`
for the endpoint described above:
```haskell
userApp :: Application
userApp = serve (Proxy :: Proxy UserApi) userServer
userServer :: Server UserApi
userServer = createUser
createUser :: Integer -> Handler User
createUser userId = do
if userId > 5000
then pure $ User { name = "some user", user_id = userId }
else throwError $ err400 { errBody = "userId is too small" }
```
### Strategy 1: Spin Up a Server, Create a Client, Make Some Requests
One of the benefits of Servant's type-level DSL for describing APIs is that
once you have provided a type-level description of your API, you can create
clients, documentation, or other tools for it somewhat magically.
In this case, we'd like to *test* our server, so we can use `servant-client`
to create a client, after which we'll run our server, and then make requests
of it and see how it responds.
Let's write some tests:
```haskell
withUserApp :: (Warp.Port -> IO ()) -> IO ()
withUserApp action =
-- testWithApplication makes sure the action is executed after the server has
-- started and is being properly shutdown.
Warp.testWithApplication (pure userApp) action
businessLogicSpec :: Spec
businessLogicSpec =
-- `around` will start our Server before the tests and turn it off after
around withUserApp $ do
-- create a test client function
let createUser = client (Proxy :: Proxy UserApi)
-- create a servant-client ClientEnv
baseUrl <- runIO $ parseBaseUrl "http://localhost"
manager <- runIO $ newManager defaultManagerSettings
let clientEnv port = mkClientEnv manager (baseUrl { baseUrlPort = port })
-- testing scenarios start here
describe "POST /user" $ do
it "should create a user with a high enough ID" $ \port -> do
result <- runClientM (createUser 50001) (clientEnv port)
result `shouldBe` (Right $ User { name = "some user", user_id = 50001})
it "will it fail with a too-small ID?" $ \port -> do
result <- runClientM (createUser 4999) (clientEnv port)
result `shouldBe` (Right $ User { name = "some user", user_id = 50001})
```
### Running These Tests
Let's run our tests and see what happens:
```
$ cabal new-test all
POST /user
should create a user with a high enough ID
should fail with a too-small ID FAILED [1]
Failures:
Testing.lhs:129:7:
1) POST /user should fail with a too-small ID
expected: Right (User {name = "some user", user_id = 50001})
but got: Left (FailureResponse (Response {responseStatusCode = Status {statusCode = 400, statusMessage = "Bad Request"}, responseHeaders = fromList [("Transfer-Encoding","chunked"),("Date","Fri, 12 Oct 2018 04:36:22 GMT"),("Server","Warp/3.2.25")], responseHttpVersion = HTTP/1.1, responseBody = "userId is too small"}))
To rerun use: --match "/POST /user/should fail with a too-small ID/"
```
Hmm. One passed and one failed! It looks like I *was* expecting a success
response in the second test, but I actually got a failure. We should fix that,
but first I'd like to introduce `hspec-wai`, which will give us different
mechanisms for making requests of our application and validating the responses
we get. We're also going to spin up a fake Elasticsearch server, so that our
server can think it's talking to a real database.
## *Mocking* 3rd Party Resources
Often our web applications will need to make their own web
requests to other 3rd-party applications. These requests provide a lot
of opportunity for failure and so we'd like to test that the right
messages and failure values (in addition to success values) are returned
from our application.
### Define the 3rd-Party Resource
With Servant's type-level API definitions, assuming you've already defined the
API you want to mock, it's relatively trivial to create a simple server for
the purposes of running tests. For instance, consider an API server that needs
to get data out of Elasticsearch. Let's first define the Elasticsearch server
and client using Servant API descriptions:
```haskell
type SearchAPI =
-- We're using Aeson's Generic JSON `Value` to make things easier on
-- ourselves. We're also representing only one Elasticsearch endpoint:
-- get item by id
"myIndex" :> "myDocType" :> Capture "docId" Integer :> Get '[JSON] Value
-- Here's our Servant Client function
getDocument = client (Proxy :: Proxy SearchAPI)
-- We can use these helpers when we want to make requests
-- using our client function
clientEnv :: Text -> Text -> IO ClientEnv
clientEnv esHost esPort = do
baseUrl <- parseBaseUrl $ unpack $ esHost <> ":" <> esPort
manager <- newManager defaultManagerSettings
pure $ mkClientEnv manager baseUrl
runSearchClient :: Text -> Text -> ClientM a -> IO (Either ClientError a)
runSearchClient esHost esPort = (clientEnv esHost esPort >>=) . runClientM
```
### Servant Server Example Using this 3rd-Party Resource
So we've got an Elasticsearch server and a client to talk to it. Let's now
build a simple app server that uses this client to retrieve documents. This
is somewhat contrived, but hopefully it illustrates the typical three-tier
application architecture.
One note: we're also going to take advantage of `lens-aeson` here, which may
look a bit foreign. The gist of it is that we're going to traverse a JSON
`Value` from Elasticsearch and try to extract some kind of document to
return.
Imagine, then, that this is our real server implementation:
```haskell
type DocApi =
"docs" :> Capture "docId" Integer :> Get '[JSON] Value
docsApp :: Text -> Text -> Application
docsApp esHost esPort = serve (Proxy :: Proxy DocApi) $ docServer esHost esPort
docServer :: Text -> Text -> Server DocApi
docServer esHost esPort = getDocById esHost esPort
-- Our Handler tries to get a doc from Elasticsearch and then tries to parse
-- it. Unfortunately, there's a lot of opportunity for failure in these
-- actions
getDocById :: Text -> Text -> Integer -> Handler Value
getDocById esHost esPort docId = do
-- Our Servant Client function returns Either ClientError Value here:
docRes <- liftIO $ runSearchClient esHost esPort (getDocument docId)
case docRes of
Left err -> throwError $ err404 { errBody = "Failed looking up content" }
Right value -> do
-- we'll either fail to parse our document or we'll return it
case value ^? _Object . ix "_source" of
Nothing -> throwError $ err400 { errBody = "Failed parsing content" }
Just obj -> pure obj
```
### Testing Our Backend
So the above represents our application and is close to a server we may
actually deploy. How then shall we test this application?
Ideally, we'd like it to make requests of a *real* Elasticsearch server, but
we certainly don't want our tests to trigger requests to a live, production
database. In addition, we don't want to depend on our real Elasticsearch
server having specific, consistent results for us to test against, because
that would make our tests flaky (and flaky tests are sometimes described as
worse than not having tests at all).
One solution to this is to create a trivial Elasticsearch server as part of
our testing code. We can do this relatively easily because we already have
an API definition for it above. With a *real* server, we can then let our own
application make requests of it and we'll simulate different scenarios in
order to make sure our application responds the way we expect it to.
Let's start with some helpers which will allow us to run a testing version
of our Elasticsearch server in another thread:
```haskell
-- | We'll run the Elasticsearch server so we can test behaviors
withElasticsearch :: IO () -> IO ()
withElasticsearch action =
bracket (liftIO $ C.forkIO $ Warp.run 9999 esTestApp)
C.killThread
(const action)
esTestApp :: Application
esTestApp = serve (Proxy :: Proxy SearchAPI) esTestServer
esTestServer :: Server SearchAPI
esTestServer = getESDocument
-- This is the *mock* handler we're going to use. We create it
-- here specifically to trigger different behavior in our tests.
getESDocument :: Integer -> Handler Value
getESDocument docId
-- arbitrary things we can use in our tests to simulate failure:
-- we want to trigger different code paths.
| docId > 1000 = throwError err500
| docId > 500 = pure . Object $ HM.fromList [("bad", String "data")]
| otherwise = pure $ Object $ HM.fromList [("_source", Object $ HM.fromList [("a", String "b")])]
```
Now, we should be ready to write some tests.
In this case, we're going to use `hspec-wai`, which will give us a simple way
to run our application, make requests, and make assertions against the
responses we receive.
Hopefully, this will simplify our testing code:
```haskell
thirdPartyResourcesSpec :: Spec
thirdPartyResourcesSpec = around_ withElasticsearch $ do
-- we call `with` from `hspec-wai` and pass *real* `Application`
with (pure $ docsApp "localhost" "9999") $ do
describe "GET /docs" $ do
it "should be able to get a document" $
-- `get` is a function from hspec-wai`.
get "/docs/1" `shouldRespondWith` 200
it "should be able to handle connection failures" $
get "/docs/1001" `shouldRespondWith` 404
it "should be able to handle parsing failures" $
get "/docs/501" `shouldRespondWith` 400
it "should be able to handle odd HTTP requests" $
-- we can also make all kinds of arbitrary custom requests to see how
-- our server responds using the `request` function:
-- request :: Method -> ByteString -> [Header]
-- -> LB.ByteString -> WaiSession SResponse
request methodPost "/docs/501" [] "{" `shouldRespondWith` 405
it "we can also do more with the Response using hspec-wai's matchers" $
-- see also `MatchHeader` and JSON-matching tools as well...
get "/docs/1" `shouldRespondWith` 200 { matchBody = MatchBody bodyMatcher }
bodyMatcher :: [Network.HTTP.Types.Header] -> Body -> Maybe String
bodyMatcher _ body = case (decode body :: Maybe Value) of
-- success in this case means we return `Nothing`
Just val | val == (Object $ HM.fromList [("a", String "b")]) -> Nothing
_ -> Just "This is how we represent failure: this message will be printed"
```
Out of the box, `hspec-wai` provides a lot of useful tools for us to run tests
against our application. What happens when we run these tests?
```
$ cabal new-test all
...
GET /docs
should be able to get a document
should be able to handle connection failures
should be able to handle parsing failures
should be able to handle odd HTTP requests
we can also do more with the Response using hspec-wai's matchers
```
Fortunately, they all passed! Let's move to another strategy: whole-API
testing.
## Servant Quickcheck
[`servant-quickcheck`](https://github.com/haskell-servant/servant-quickcheck)
is a project that allows users to write tests for whole Servant APIs using
quickcheck-style property-checking mechanisms.
`servant-quickcheck` is great for asserting API-wide rules, such as "no
endpoint throws a 500" or "all 301 status codes also come with a Location
header". The project even comes with a number of predicates that reference
the [RFCs they originate from](https://github.com/haskell-servant/servant-quickcheck/blob/master/src/Servant/QuickCheck/Internal/Predicates.hs).
In other words, it's one way to assert that your APIs conform to specs and
best practices.
### Quickcheckable API
Let's make an API and a server to demonstrate how to use `servant-quickcheck`:
```haskell
type API = ReqBody '[JSON] String :> Post '[JSON] String
:<|> Get '[JSON] Int
:<|> BasicAuth "some-realm" () :> Get '[JSON] ()
api :: Proxy API
api = Proxy
server :: IO (Server API)
server = do
mvar <- newMVar ""
return $ (\x -> liftIO $ swapMVar mvar x)
:<|> (liftIO $ readMVar mvar >>= return . length)
:<|> (const $ return ())
```
### Using `servant-quickcheck`
Let's build some tests for our API using `servant-quickcheck`.
Similar to the above examples, we're going to create `Spec`s, but in this
case, we'll rely on a number of predicates available from `servant-quickcheck`
to see if our API server conforms to best practices:
```haskell
-- Let's set some QuickCheck values
args :: Args
args = defaultArgs { maxSuccess = 500 }
-- Here's a Servant Context object we'll use
ctx :: Context '[BasicAuthCheck ()]
ctx = BasicAuthCheck (const . return $ NoSuchUser) :. EmptyContext
servantQuickcheckSpec :: Spec
servantQuickcheckSpec = describe "" $ do
it "API demonstrates best practices" $
-- `withServerServer` and `withServantServerAndContext` come from `servant-quickcheck`
withServantServerAndContext api ctx server $ \burl ->
-- `serverSatisfies` and the predicates also come from `servant-quickcheck`
serverSatisfies api burl args (unauthorizedContainsWWWAuthenticate
<%> not500
<%> onlyJsonObjects -- this one isn't true!
<%> mempty)
it "API doesn't have these things implemented yet" $
withServantServerAndContext api ctx server $ \burl -> do
serverDoesntSatisfy api burl args (getsHaveCacheControlHeader
<%> notAllowedContainsAllowHeader
<%> mempty)
```
Let's see what happens when we run these tests:
```
API demonstrates best practices FAILED [2]
+++ OK, passed 500 tests.
API doesn't have these things implemented yet
src/Servant/QuickCheck/Internal/QuickCheck.hs:143:11:
2) Main[339:25] API demonstrates best practices
Failed:
Just Predicate failed
Predicate: onlyJsonObjects
Response:
Status code: 200
Headers: "Transfer-Encoding": "chunked"
"Date": "Fri, 12 Oct 2018 04:36:22 GMT"
"Server": "Warp/3.2.25"
"Content-Type": "application/json;charset=utf-8"
Body: ""
To rerun use: --match "/Main[339:25]/API demonstrates best practices/"
Randomized with seed 1046277487
Finished in 0.4306 seconds
```
Hmm. It looks like we *thought* our API only returned JSON objects, which is a
best practice, but in fact, we *did* have an endpoint that returned an empty
body, which you can see in the printed response above: `Body: ""`. We should
consider revising our API to only return top-level JSON Objects in the future!
### Other Cool Things
`servant-quickcheck` also has a cool mechanism where you can compare two API
servers to demonstrate that they respond identically to requests. This may be
useful if you are planning to rewrite one API in another language or with
another web framework. You have to specify whether you're looking for
`jsonEquality` vs regular `ByteString` equality, though.
## Conclusion
There are lots of techniques for testing and we only covered a few here.
Useful libraries such as `hspec-wai` have ways of running Wai `Application`s
and sending requests to them, while Servant's type-level DSL for defining APIs
allows us to more easily mock out servers and to derive clients, which will
only craft valid requests.
Lastly, if you want a broad overview of where your application fits in with
regard to best practices, consider using `servant-quickcheck`.
This program is available as a cabal project
[here](https://github.com/haskell-servant/servant/tree/master/doc/cookbook/testing).

View file

@ -1,38 +0,0 @@
cabal-version: 2.2
name: cookbook-testing
version: 0.0.1
synopsis: Common testing patterns in Servant apps
description: This recipe includes various strategies for writing tests for Servant.
homepage: http://docs.servant.dev/
license: BSD-3-Clause
license-file: ../../../servant/LICENSE
author: Servant Contributors
maintainer: haskell-servant-maintainers@googlegroups.com
category: Servant
build-type: Simple
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
executable cookbook-testing
main-is: Testing.lhs
build-depends: base == 4.*
, base-compat
, text >= 1.2
, aeson >= 1.2
, lens-aeson
, lens
, servant
, servant-client
, servant-server
, servant-quickcheck >= 0.0.10
, http-client
, http-types >= 0.12
, hspec
, hspec-wai
, QuickCheck
, unordered-containers
, warp >= 3.2
, wai >= 3.2
, wai-extra
default-language: Haskell2010
ghc-options: -Wall -pgmL markdown-unlit
build-tool-depends: markdown-unlit:markdown-unlit

View file

@ -1,120 +0,0 @@
# Using a custom monad
In this section we will create an API for a book shelf without any backing DB storage.
We will keep state in memory and share it between requests using `Reader` monad and `STM`.
We start with a pretty standard set of imports and definition of the model:
``` haskell
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TypeOperators #-}
import Control.Concurrent (forkIO, killThread)
import Control.Concurrent.STM.TVar (TVar, newTVar, readTVar,
writeTVar)
import Control.Exception (bracket)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.STM (atomically)
import Control.Monad.Trans.Reader (ReaderT, ask, runReaderT)
import Data.Aeson (FromJSON, ToJSON)
import GHC.Generics (Generic)
import Network.HTTP.Client (defaultManagerSettings,
newManager)
import Network.Wai.Handler.Warp (run)
import Servant
import Servant.Client
newtype Book = Book String deriving (Show, Generic)
instance ToJSON Book
instance FromJSON Book
```
Now, let's define the API for book storage.
For the sake of simplicity we'll only have methods for getting all books and adding a new one.
``` haskell
type GetBooks = Get '[JSON] [Book]
type AddBook = ReqBody '[JSON] Book :> PostCreated '[JSON] Book
type BooksAPI = "books" :> (GetBooks :<|> AddBook)
api :: Proxy BooksAPI
api = Proxy
```
Next, we define the state and the monad to run our handlers
``` haskell
data State = State
{ books :: TVar [Book]
}
type AppM = ReaderT State Handler
```
Note that we can't use `State` monad here, because state will not be shared between requests.
We can now define handlers in terms of `AppM`...
```haskell
server :: ServerT BooksAPI AppM
server = getBooks :<|> addBook
where getBooks :: AppM [Book]
getBooks = do
State{books = p} <- ask
liftIO $ atomically $ readTVar p
addBook :: Book -> AppM Book
addBook book = do
State{books = p} <- ask
liftIO $ atomically $ readTVar p >>= writeTVar p . (book :)
return book
```
...and transform `AppM` to `Handler` by simply using `runReaderT`
``` haskell
nt :: State -> AppM a -> Handler a
nt s x = runReaderT x s
app :: State -> Application
app s = serve api $ hoistServer api (nt s) server
```
Finally, we end up with the following program
``` haskell
main :: IO ()
main = do
let port = 8080
mgr <- newManager defaultManagerSettings
initialBooks <- atomically $ newTVar []
let runApp = run port $ app $ State initialBooks
bracket (forkIO runApp) killThread $ \_ -> do
let getBooksClient :<|> addBookClient = client api
let printBooks = getBooksClient >>= liftIO . print
_ <- flip runClientM (mkClientEnv mgr (BaseUrl Http "localhost" port "")) $ do
_ <- printBooks
_ <- addBookClient $ Book "Harry Potter and the Order of the Phoenix"
_ <- printBooks
_ <- addBookClient $ Book "To Kill a Mockingbird"
_ <- printBooks
_ <- addBookClient $ Book "The Picture of Dorian Gray"
printBooks
return ()
```
When run, it outputs the following:
```
Running cookbook-using-custom-monad...
[]
[Book "Harry Potter and the Order of the Phoenix"]
[Book "To Kill a Mockingbird",Book "Harry Potter and the Order of the Phoenix"]
[Book "The Picture of Dorian Gray",Book "To Kill a Mockingbird",Book "Harry Potter and the Order of the Phoenix"]
```
To use `Raw` endpoints, look at the
[servant-rawm](http://hackage.haskell.org/package/servant-rawm) package.

View file

@ -1,28 +0,0 @@
cabal-version: 2.2
name: cookbook-using-custom-monad
version: 0.1
synopsis: Using custom monad to pass a state between handlers
homepage: http://docs.servant.dev/
license: BSD-3-Clause
license-file: ../../../servant/LICENSE
author: Servant Contributors
maintainer: haskell-servant-maintainers@googlegroups.com
build-type: Simple
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
executable cookbook-using-custom-monad
main-is: UsingCustomMonad.lhs
build-depends: base == 4.*
, aeson >= 1.2
, servant
, servant-client
, servant-server
, warp >= 3.2
, wai >= 3.2
, http-client >= 0.5
, markdown-unlit >= 0.4
, stm >= 2.4
, transformers >= 0.3
default-language: Haskell2010
ghc-options: -Wall -pgmL markdown-unlit
build-tool-depends: markdown-unlit:markdown-unlit

View file

@ -1,192 +0,0 @@
# Inspecting, debugging, simulating clients and more
or simply put: _a practical introduction to `Servant.Client.Free`_.
Someone asked on IRC how one could access the intermediate Requests (resp. Responses)
produced (resp. received) by client functions derived using servant-client.
My response to such inquiries is: to extend `servant-client` in an ad-hoc way (e.g for testing or debugging
purposes), use `Servant.Client.Free`. This recipe shows how.
First the module header, but this time We'll comment the imports.
```haskell
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
module Main (main) where
```
We will primarily use `Servant.Client.Free`, it doesn't re-export anything
from `free` package, so we need to import it as well.
```haskell
import Control.Monad.Free
import Servant.Client.Free
```
Also we'll use `servant-client` internals, which uses `http-client`,
so let's import them *qualified*
```haskell
import qualified Servant.Client.Internal.HttpClient as I
import qualified Network.HTTP.Client as HTTP
```
The rest of the imports are for a server we implement here for completeness.
```haskell
import Servant
import Network.Wai.Handler.Warp (run)
import System.Environment (getArgs)
```
## API & Main
We'll work with a very simple API:
```haskell
type API = "square" :> Capture "n" Int :> Get '[JSON] Int
api :: Proxy API
api = Proxy
```
Next we implement a `main`. If passed `"server"` it will run `server`, if passed
`"client"` it will run a `test` function (to be defined next). This should be
pretty straightforward:
```haskell
main :: IO ()
main = do
args <- getArgs
case args of
("server":_) -> do
putStrLn "Starting cookbook-using-free-client at http://localhost:8000"
run 8000 $ serve api $ \n -> return (n * n)
("client":_) ->
test
_ -> do
putStrLn "Try:"
putStrLn "cabal new-run cookbook-using-free-client server"
putStrLn "cabal new-run cookbook-using-free-client client"
```
## Test
In the client part, we will use a `Servant.Client.Free` client.
Because we have a single endpoint API, we'll get a single client function,
running in the `Free ClientF` (free) monad:
```haskell
getSquare :: Int -> Free ClientF Int
getSquare = client api
```
Such clients are "client functions without a backend", so to speak,
or where the backend has been abstracted out. To be more precise, `ClientF` is a functor that
precisely represents the operations servant-client-core needs from an http client backend.
So if we are to emulate one or augment what such a backend does, it will be by interpreting
all those operations, the way we want to. This also means we get access to the requests and
responses and can do anything we want with them, right when they are produced or consumed,
respectively.
Next, we can write our small test. We'll pass a value to `getSquare` and inspect
the `Free` structure. The first three possibilities are self-explanatory:
```haskell
test :: IO ()
test = case getSquare 42 of
Pure n ->
putStrLn $ "ERROR: got pure result: " ++ show n
Free (Throw err) ->
putStrLn $ "ERROR: got error right away: " ++ show err
```
We are interested in `RunRequest`, that's what client should block on:
```haskell
Free (RunRequest req k) -> do
```
Then we need to prepare the context, get HTTP (connection) `Manager`
and `BaseUrl`:
```haskell
burl <- parseBaseUrl "http://localhost:8000"
mgr <- HTTP.newManager HTTP.defaultManagerSettings
```
Now we can use `servant-client`'s internals to convert servant's `Request`
to http-client's `Request`, and we can inspect it:
```haskell
req' <- I.defaultMakeClientRequest burl req
putStrLn $ "Making request: " ++ show req'
```
`servant-client`'s request does a little more, but this is good enough for
our demo. We get back an http-client `Response` which we can also inspect.
```haskell
res' <- HTTP.httpLbs req' mgr
putStrLn $ "Got response: " ++ show res'
```
And we continue by turning http-client's `Response` into servant's `Response`,
and calling the continuation. We should get a `Pure` value.
```haskell
let res = I.clientResponseToResponse id res'
case k res of
Pure n ->
putStrLn $ "Expected 1764, got " ++ show n
_ ->
putStrLn "ERROR: didn't get a response"
```
So that's it. Using `Free` we can evaluate servant clients step-by-step, and
validate that the client functions or the HTTP client backend does what we expect
(e.g by printing requests/responses on the fly). In fact, using `Servant.Client.Free`
is a little simpler than defining a custom `RunClient` instance, especially
for those cases where it is handy to have the full sequence of client calls
and responses available for us to inspect, since `RunClient` only gives us
access to one `Request` or `Response` at a time.
On the other hand, a "batch collection" of requests and/or responses can be achieved
with both free clients and a custom `RunClient` instance rather easily, for example
by using a `Writer [(Request, Response)]` monad.
Here is an example of running our small `test` against a running server:
```
Making request: Request {
host = "localhost"
port = 8000
secure = False
requestHeaders = [("Accept","application/json;charset=utf-8,application/json")]
path = "/square/42"
queryString = ""
method = "GET"
proxy = Nothing
rawBody = False
redirectCount = 10
responseTimeout = ResponseTimeoutDefault
requestVersion = HTTP/1.1
}
Got response: Response
{ responseStatus = Status {statusCode = 200, statusMessage = "OK"}
, responseVersion = HTTP/1.1
, responseHeaders =
[ ("Transfer-Encoding","chunked")
, ("Date","Thu, 05 Jul 2018 21:12:41 GMT")
, ("Server","Warp/3.2.22")
, ("Content-Type","application/json;charset=utf-8")
]
, responseBody = "1764"
, responseCookieJar = CJ {expose = []}
, responseClose' = ResponseClose
}
Expected 1764, got 1764
```

View file

@ -1,26 +0,0 @@
cabal-version: 2.2
name: cookbook-using-free-client
version: 0.1
synopsis: Using Free client
homepage: http://docs.servant.dev/
license: BSD-3-Clause
license-file: ../../../servant/LICENSE
author: Servant Contributors
maintainer: haskell-servant-maintainers@googlegroups.com
build-type: Simple
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
executable cookbook-using-free-client
main-is: UsingFreeClient.lhs
build-depends: base >= 4.9 && <5
, free
, servant
, servant-client
, http-client
, servant-client-core
, base-compat
, servant-server
, warp >= 3.2
default-language: Haskell2010
ghc-options: -Wall -pgmL markdown-unlit
build-tool-depends: markdown-unlit:markdown-unlit >= 0.4

View file

@ -1,223 +0,0 @@
# 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.
## Compatibility
:warning: This cookbook is compatible with GHC 8.6.1 or higher :warning:
## Preliminaries
```haskell
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingVia #-}
{-# 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 Control.Monad (when)
import Control.Monad.Except (ExceptT (..), MonadError (..), MonadTrans (..), runExceptT)
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.Client
import Servant.Server
import Servant.Swagger
```
## 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 $ foldMapUnion (Proxy @Show) show <$> result
print $ matchUnion @FisxUser <$> result
print $ matchUnion @(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 (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
data Foo = Foo Int Int Int
deriving (Show, Eq, GHC.Generic, ToJSON)
deriving HasStatus via WithStatus 200 Foo
data Bar = Bar
deriving (Show, Eq, GHC.Generic)
instance ToJSON Bar
h :: Handler (Union '[Foo, WithStatus 400 Bar])
h = runUVerbT $ do
when ({- something bad -} True) $
throwUVerb $ WithStatus @400 Bar
when ({- really bad -} False) $
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 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.)
```haskell
main :: IO ()
main = return ()
```

View file

@ -1,35 +0,0 @@
cabal-version: 2.2
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: BSD-3-Clause
license-file: ../../../servant/LICENSE
author: Servant Contributors
maintainer: haskell-servant-maintainers@googlegroups.com
category: Servant
build-type: Simple
tested-with: GHC==8.6.5, GHC==8.8.4, GHC==8.10.7
executable cookbook-uverb
main-is: UVerb.lhs
build-depends: base == 4.*
, aeson >= 1.2
, aeson-pretty >= 0.8.8
, async
, http-client
, mtl
, servant
, servant-client
, servant-server
, servant-swagger
, string-conversions
, swagger2
, wai
, warp
if impl(ghc >= 9)
buildable: False
default-language: Haskell2010
ghc-options: -Wall -pgmL markdown-unlit
build-tool-depends: markdown-unlit:markdown-unlit

View file

@ -1,60 +0,0 @@
# Example Projects
- **[example-servant-minimal](https://github.com/haskell-servant/example-servant-minimal)**:
A minimal example for a web server written using **servant-server**,
including a test-suite using [**hspec**](http://hspec.github.io/) and
**servant-client**.
- **[servant-examples](https://github.com/sras/servant-examples)**:
Similar to [the cookbook](https://docs.servant.dev/en/latest/cookbook/index.html) but
with no explanations, for developers who just want to look at code examples to find out how to do X or Y
with servant.
- **[stack-templates](https://github.com/commercialhaskell/stack-templates)**
Repository for templates for haskell projects, including some templates using
**servant**. These templates can be used with `stack new`.
- **[custom-monad](https://github.com/themoritz/diener)**:
A custom monad that can replace `IO` in servant applications. It adds among
other things logging functionality and a reader monad (for database connections).
A full usage example of servant/diener is also provided.
- **[example-servant-elm](https://github.com/haskell-servant/example-servant-elm)**:
An example for a project consisting of
- a backend web server written using **servant-server**,
- a frontend written in [elm](http://elm-lang.org/) using
[servant-elm](https://github.com/mattjbray/servant-elm) to generate client
functions in elm for the API,
- test-suites for both the backend and the frontend.
- **[servant-purescript](https://github.com/eskimor/servant-purescript/tree/master/examples/central-counter)**:
An example consisting of
- a backend that uses `servant`
- a frontend written in [PureScript](http://www.purescript.org/) using
[servant-purescript](https://github.com/eskimor/servant-purescript) to generate
an API wrapper in PureScript to interface the web API with
- **[example-servant-persistent](https://github.com/haskell-servant/example-servant-persistent)**:
An example for a web server written with **servant-server** and
[persistent](https://www.stackage.org/package/persistent) for writing data
into a database.
- **[full-example-servant-elm-auth-yeshql-postgresql](https://github.com/aRkadeFR/FlashCard)**:
A full open source website written with **servant-server**, yeshql, postgresql and elm 0.19.
- [`import Servant` github search](https://github.com/search?q=%22import+Servant%22+language%3AHaskell&type=Code)
It has thousands of results and can be a good way to see how people use servant in their projects or even to discover
servant-related libraries.

View file

@ -3,24 +3,20 @@ servant A Type-Level Web DSL
.. image:: https://raw.githubusercontent.com/haskell-servant/servant/master/servant.png
**servant** is a set of Haskell libraries for writing *type-safe* web
applications but also *deriving* clients (in Haskell and other languages) or
generating documentation for them, and more.
**servant** is a set of packages for declaring web APIs at the type-level and
then using those API specifications to:
This is achieved by taking as input a description of the web API
as a Haskell type. Servant is then able to check that your server-side request
handlers indeed implement your web API faithfully, or to automatically derive
Haskell functions that can hit a web application that implements this API,
generate a Swagger description or code for client functions in some other
languages directly.
- write servers (this part of **servant** can be considered a web framework),
- obtain client functions (in haskell),
- generate client functions for other programming languages,
- generate documentation for your web applications
- and more...
If you would like to learn more, click the tutorial link below.
All in a type-safe manner.
.. toctree::
:maxdepth: 2
introduction.rst
tutorial/index.rst
cookbook/index.rst
examples.md
links.rst
principles.rst

View file

@ -1,5 +1,5 @@
Principles
----------
Introduction
------------
**servant** has the following guiding principles:
@ -22,7 +22,7 @@ Principles
- separation of concerns
Your handlers and your HTTP logic should be separate. True to the philosophy
Your handlers and your HTTP logic should be separate. True to the philosphy
at the core of HTTP and REST, with **servant** your handlers return normal
Haskell datatypes - that's the resource. And then from a description of your
API, **servant** handles the *presentation* (i.e., the Content-Types). But

View file

@ -3,7 +3,7 @@ Helpful Links
-------------
- the central documentation (this site):
`docs.servant.dev <http://docs.servant.dev/>`_
`haskell-servant.readthedocs.org <http://haskell-servant.readthedocs.org/>`_
- the github repo:
`github.com/haskell-servant/servant <https://github.com/haskell-servant/servant>`_
@ -12,13 +12,13 @@ Helpful Links
`https://github.com/haskell-servant/servant/issues <https://github.com/haskell-servant/servant/issues>`_
- the irc channel:
`#haskell-servant on libera.chat <https://web.libera.chat/#haskell-servant>`_
``#servant`` on freenode
- the mailing list:
`groups.google.com/forum/#!forum/haskell-servant <https://groups.google.com/forum/#!forum/haskell-servant>`_
- blog posts and videos and slides of some talks on servant:
`www.servant.dev <http://www.servant.dev>`_
`haskell-servant.github.io <http://haskell-servant.github.io>`_
- the servant packages on hackage:

View file

@ -1,4 +1,25 @@
recommonmark==0.5.0
Sphinx==1.8.4
sphinx_rtd_theme>=0.4.2
jinja2<3.1.0
alabaster==0.7.7
argh==0.26.1
Babel==2.2.0
backports-abc==0.4
backports.ssl-match-hostname==3.5.0.1
certifi==2015.11.20.1
CommonMark==0.5.4
docutils==0.12
Jinja2==2.8
livereload==2.4.1
MarkupSafe==0.23
pathtools==0.1.2
Pygments==2.1.1
pytz==2015.7
PyYAML==3.11
recommonmark==0.4.0
singledispatch==3.4.0.3
six==1.10.0
snowballstemmer==1.2.1
Sphinx==1.3.4
sphinx-autobuild==0.5.2
sphinx-rtd-theme==0.1.9
tornado==4.3
watchdog==0.8.3
wheel==0.26.0

View file

@ -10,7 +10,6 @@ need to have some language extensions and imports:
module ApiType where
import Data.Text
import Data.Time (UTCTime)
import Servant.API
```
@ -35,9 +34,7 @@ data SortBy = Age | Name
data User = User {
name :: String,
age :: Int,
email :: String,
registration_date :: UTCTime
age :: Int
}
```
@ -61,14 +58,6 @@ Let's break that down:
is equivalent to `/list-all/users`. This means that sometimes `:>` is somehow
equivalent to `/`, but sometimes it just lets you chain another combinator.
Tip: If your endpoint responds to `/` (the root path), just omit any combinators
that introduce path segments. E.g. the following api has only one endpoint on `/`:
``` haskell
type RootEndpoint =
Get '[JSON] User
```
We can also describe APIs with multiple endpoints by using the `:<|>`
combinators. Here's an example:
@ -79,7 +68,7 @@ type UserAPI2 = "users" :> "list-all" :> Get '[JSON] [User]
**servant** provides a fair amount of combinators out-of-the-box, but you can
always write your own when you need it. Here's a quick overview of the most
often needed combinators that **servant** comes with.
often needed the combinators that **servant** comes with.
## Combinators
@ -127,26 +116,6 @@ type UserAPI4 = "users" :> Get '[JSON] [User]
:<|> "admins" :> Get '[JSON] [User]
```
### `StreamGet` and `StreamPost`
*Note*: Streaming has changed considerably in `servant-0.15`.
The `StreamGet` and `StreamPost` combinators are defined in terms of the more general `Stream`
``` haskell ignore
data Stream (method :: k1) (status :: Nat) (framing :: *) (contentType :: *) (a :: *)
type StreamGet = Stream 'GET 200
type StreamPost = Stream 'POST 200
```
These describe endpoints that return a stream of values rather than just a
single value. They not only take a single content type as a parameter, but also
a framing strategy -- this specifies how the individual results are delineated
from one another in the stream. The three standard strategies given with
Servant are `NewlineFraming`, `NetstringFraming` and `NoFraming`, but others
can be written to match other protocols.
### `Capture`
URL captures are segments of the path of a URL that are variable and whose actual value is
@ -177,12 +146,13 @@ type UserAPI5 = "user" :> Capture "userid" Integer :> Get '[JSON] User
-- except that we explicitly say that "userid"
-- must be an integer
:<|> "user" :> Capture "userid" Integer :> DeleteNoContent
:<|> "user" :> Capture "userid" Integer :> DeleteNoContent '[JSON] NoContent
-- equivalent to 'DELETE /user/:userid'
```
In the second case, `DeleteNoContent` specifies a 204 response code
and that the response will always be empty.
In the second case, `DeleteNoContent` specifies a 204 response code,
`JSON` specifies the content types on which the handler will match,
and `NoContent` says that the response will always be empty.
### `QueryParam`, `QueryParams`, `QueryFlag`
@ -327,7 +297,7 @@ Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication).
When protecting endpoints with basic authentication, we need to specify two items:
1. The **realm** of authentication as per the Basic Authentication spec.
1. The **realm** of authentication as per the Basic Authentictaion spec.
2. The datatype returned by the server after authentication is verified. This
is usually a `User` or `Customer` type datatype.
@ -340,33 +310,11 @@ data BasicAuth (realm :: Symbol) (userData :: *)
Which is used like so:
``` haskell
type ProtectedAPI11
= UserAPI -- this is public
:<|> BasicAuth "my-realm" User :> UserAPI2 -- this is protected by auth
type ProtectedAPI12
= UserAPI -- this is public
:<|> BasicAuth "my-real" User :> UserAPI2 -- this is protected by auth
```
### Empty APIs
Sometimes it is useful to be able to generalise an API over the type of some
part of it:
``` haskell
type UserAPI12 innerAPI
= UserAPI -- this is the fixed bit of the API
:<|> "inner" :> innerAPI -- this lets us put various other APIs under /inner
```
If there is a case where you do not have anything extra to serve, you can use
the `EmptyAPI` combinator to indicate this:
``` haskell
type UserAPI12Alone = UserAPI12 EmptyAPI
```
This also works well as a placeholder for unfinished parts of an API while it
is under development, for when you know that there should be _something_ there
but you don't yet know what. Think of it as similar to the unit type `()`.
### Interoperability with `wai`: `Raw`
Finally, we also include a combinator named `Raw` that provides an escape hatch
@ -375,7 +323,7 @@ you want to plug a [wai `Application`](http://hackage.haskell.org/package/wai)
into your webservice:
``` haskell
type UserAPI13 = "users" :> Get '[JSON] [User]
type UserAPI11 = "users" :> Get '[JSON] [User]
-- a /users endpoint
:<|> Raw
@ -389,30 +337,3 @@ One example for this is if you want to serve a directory of static files along
with the rest of your API. But you can plug in everything that is an
`Application`, e.g. a whole web application written in any of the web
frameworks that support `wai`.
Be mindful! The `servant-server`'s router works by pattern-matching the
different routes that are composed using `:<|>`. `Raw`, as an escape hatch,
matches any route that hasn't been matched by previous patterns. Therefore,
any subsequent route will be silently ignored.
``` haskell
type UserAPI14 = Raw
:<|> "users" :> Get '[JSON] [User]
-- In this situation, the /users endpoint
-- will not be reachable because the Raw
-- endpoint matches requests before
```
A simple way to avoid this pitfall is to either use `Raw` as the last
definition, or to always have it under a static path.
``` haskell
type UserAPI15 = "files" :> Raw
-- The raw endpoint is under the /files
-- static path, so it won't match /users.
:<|> "users" :> Get '[JSON] [User]
type UserAPI16 = "users" :> Get '[JSON] [User]
:<|> Raw
-- The Raw endpoint is matched last, so
-- it won't overlap another endpoint.
```

View file

@ -44,9 +44,11 @@ You can use this combinator to protect an API as follows:
module Authentication where
import Control.Monad.Trans.Except (ExceptT, throwE)
import Data.Aeson (ToJSON)
import Data.ByteString (ByteString)
import Data.Map (Map, fromList)
import Data.Monoid ((<>))
import qualified Data.Map as Map
import Data.Proxy (Proxy (Proxy))
import Data.Text (Text)
@ -57,18 +59,16 @@ import Servant.API ((:<|>) ((:<|>)), (:>), BasicAuth,
Get, JSON)
import Servant.API.BasicAuth (BasicAuthData (BasicAuthData))
import Servant.API.Experimental.Auth (AuthProtect)
import Servant (throwError)
import Servant.Server (BasicAuthCheck (BasicAuthCheck),
BasicAuthResult( Authorized
, Unauthorized
),
Context ((:.), EmptyContext),
err401, err403, errBody, Server,
serveWithContext, Handler)
ServantErr, serveWithContext)
import Servant.Server.Experimental.Auth (AuthHandler, AuthServerData,
mkAuthHandler)
import Servant.Server.Experimental.Auth()
import Web.Cookie (parseCookies)
-- | private data that needs protection
newtype PrivateData = PrivateData { ssshhh :: Text }
@ -107,7 +107,7 @@ API with "private." Additionally, the private parts of our API use the
realm for this authentication is `"foo-realm"`).
Unfortunately we're not done. When someone makes a request to our `"private"`
API, we're going to need to provide to servant the logic for validating
API, we're going to need to provide to servant the logic for validifying
usernames and passwords. This adds a certain conceptual wrinkle in servant's
design that we'll briefly discuss. If you want the **TL;DR**: we supply a lookup
function to servant's new `Context` primitive.
@ -117,22 +117,22 @@ or dictated the structure of a response (e.g. a `Capture` param is pulled from
the request path). Now consider an API resource protected by basic
authentication. Once the required `WWW-Authenticate` header is checked, we need
to verify the username and password. But how? One solution would be to force an
API author to provide a function of type `BasicAuthData -> Handler User`
API author to provide a function of type `BasicAuthData -> ExceptT ServantErr IO User`
and servant should use this function to authenticate a request. Unfortunately
this didn't work prior to `0.5` because all of servant's machinery was
engineered around the idea that each combinator can extract information from
only the request. We cannot extract the function
`BasicAuthData -> Handler User` from a request! Are we doomed?
`BasicAuthData -> ExceptT ServantErr IO User` from a request! Are we doomed?
Servant `0.5` introduced `Context` to handle this. The type machinery is beyond
the scope of this tutorial, but the idea is simple: provide some data to the
`serve` function, and that data is propagated to the functions that handle each
combinator. Using `Context`, we can supply a function of type
`BasicAuthData -> Handler User` to the `BasicAuth` combinator
`BasicAuthData -> ExceptT ServantErr IO User` to the `BasicAuth` combinator
handler. This will allow the handler to check authentication and return a `User`
to downstream handlers if successful.
In practice we wrap `BasicAuthData -> Handler User` into a slightly
In practice we wrap `BasicAuthData -> ExceptT ServantErr IO` into a slightly
different function to better capture the semantics of basic authentication:
``` haskell ignore
@ -173,7 +173,7 @@ And now we create the `Context` used by servant to find `BasicAuthCheck`:
```haskell
-- | We need to supply our handlers with the right Context. In this case,
-- Basic Authentication requires a Context Entry with the 'BasicAuthCheck' value
-- tagged with "foo-tag" This context is then supplied to 'server' and threaded
-- tagged with "foo-tag" This context is then supplied to 'server' and threaded
-- to the BasicAuth HasServer handlers.
basicAuthServerContext :: Context (BasicAuthCheck User ': '[])
basicAuthServerContext = authCheck :. EmptyContext
@ -245,61 +245,53 @@ your feedback!
### What is Generalized Authentication?
**TL;DR**: you throw a tagged `AuthProtect` combinator in front of the
endpoints you want protected and then supply a function `Request -> Handler a`,
where `a` is the type of your choice representing the data returned by
successful authentication - e.g., a `User` or, in our example below, `Account`.
This function is run anytime a request matches a protected endpoint. It
precisely solves the "I just need to protect these endpoints with a function
that does some complicated business logic" and nothing more. Behind the scenes
we use a type family instance (`AuthServerData`) and `Context` to accomplish
this.
**TL;DR**: you throw a tagged `AuthProtect` combinator in front of the endpoints
you want protected and then supply a function `Request -> ExceptT IO ServantErr user`
which we run anytime a request matches a protected endpoint. It precisely solves
the "I just need to protect these endpoints with a function that does some
complicated business logic" and nothing more. Behind the scenes we use a type
family instance (`AuthServerData`) and `Context` to accomplish this.
### Generalized Authentication in Action
Let's implement a trivial authentication scheme. We will protect our API by
looking for a cookie named `"servant-auth-cookie"`. This cookie's value will
contain a key from which we can lookup an `Account`.
contain a key from which we can lookup a `User`.
```haskell
-- | An account type that we "fetch from the database" after
-- | A user type that we "fetch from the database" after
-- performing authentication
newtype Account = Account { unAccount :: Text }
-- | A (pure) database mapping keys to accounts.
-- | A (pure) database mapping keys to users.
database :: Map ByteString Account
database = fromList [ ("key1", Account "Anne Briggs")
, ("key2", Account "Bruce Cockburn")
, ("key3", Account "Ghédalia Tazartès")
]
-- | A method that, when given a password, will return an Account.
-- | A method that, when given a password, will return a Account.
-- This is our bespoke (and bad) authentication logic.
lookupAccount :: ByteString -> Handler Account
lookupAccount :: ByteString -> ExceptT ServantErr IO Account
lookupAccount key = case Map.lookup key database of
Nothing -> throwError (err403 { errBody = "Invalid Cookie" })
Nothing -> throwE (err403 { errBody = "Invalid Cookie" })
Just usr -> return usr
```
For generalized authentication, servant exposes the `AuthHandler` type,
which is used to wrap the `Request -> Handler Account` logic. Let's
which is used to wrap the `Request -> ExceptT IO ServantErr user` logic. Let's
create a value of type `AuthHandler Request Account` using the above `lookupAccount`
method (note: we depend upon [`cookie`](https://hackage.haskell.org/package/cookie)'s
`parseCookies` for this):
method:
```haskell
--- | The auth handler wraps a function from Request -> Handler Account.
--- We look for a token in the request headers that we expect to be in the cookie.
--- The token is then passed to our `lookupAccount` function.
-- | The auth handler wraps a function from Request -> ExceptT ServantErr IO Account
-- we look for a Cookie and pass the value of the cookie to `lookupAccount`.
authHandler :: AuthHandler Request Account
authHandler = mkAuthHandler handler
where
maybeToEither e = maybe (Left e) Right
throw401 msg = throwError $ err401 { errBody = msg }
handler req = either throw401 lookupAccount $ do
cookie <- maybeToEither "Missing cookie header" $ lookup "cookie" $ requestHeaders req
maybeToEither "Missing token in cookie" $ lookup "servant-auth-cookie" $ parseCookies cookie
authHandler =
let handler req = case lookup "servant-auth-cookie" (requestHeaders req) of
Nothing -> throwE (err401 { errBody = "Missing auth header" })
Just authCookieKey -> lookupAccount authCookieKey
in mkAuthHandler handler
```
Let's now protect our API with our new, bespoke authentication scheme. We'll
@ -317,7 +309,7 @@ genAuthAPI = Proxy
Now we need to bring everything together for the server. We have the
`AuthHandler Request Account` value and an `AuthProtected` endpoint. To bind these
together, we need to provide a [Type Family](https://downloads.haskell.org/~ghc/8.8.1/docs/html/users_guide/glasgow_exts.html#type-families)
together, we need to provide a [Type Family](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/type-families.html)
instance that tells the `HasServer` instance that our `Context` will supply a
`Account` (via `AuthHandler Request Account`) and that downstream combinators will
have access to this `Account` value (or an error will be thrown if authentication
@ -337,7 +329,7 @@ We now construct the `Context` for our server, allowing us to instantiate a
value of type `Server AuthGenAPI`, in addition to the server value:
```haskell
-- | The context that will be made available to request handlers. We supply the
-- | The context that will be made available to request handlers. We supply the
-- "cookie-auth"-tagged request handler defined above, so that the 'HasServer' instance
-- of 'AuthProtect' can extract the handler and run it on the request.
genAuthServerContext :: Context (AuthHandler Request Account ': '[])
@ -345,7 +337,7 @@ genAuthServerContext = authHandler :. EmptyContext
-- | Our API, where we provide all the author-supplied handlers for each end
-- point. Note that 'privateDataFunc' is a function that takes 'Account' as an
-- argument. We don't worry about the authentication instrumentation here,
-- argument. We dont' worry about the authentication instrumentation here,
-- that is taken care of by supplying context
genAuthServer :: Server AuthGenAPI
genAuthServer =
@ -367,10 +359,10 @@ genAuthMain = run 8080 (serveWithContext genAuthAPI genAuthServerContext genAuth
$ curl -XGET localhost:8080/private
Missing auth header
$ curl -XGET localhost:8080/private -H "Cookie: servant-auth-cookie=key3"
$ curl -XGET localhost:8080/private -H "servant-auth-cookie: key3"
[{"ssshhh":"this is a secret: Ghédalia Tazartès"}]
$ curl -XGET localhost:8080/private -H "Cookie: servant-auth-cookie=bad-key"
$ curl -XGET localhost:8080/private -H "servant-auth-cookie: bad-key"
Invalid Cookie
$ curl -XGET localhost:8080/public
@ -384,14 +376,14 @@ Creating a generalized, ad-hoc authentication scheme was fairly straight
forward:
1. use the `AuthProtect` combinator to protect your API.
2. choose an application-specific data type used by your server when
authentication is successful (in our case this was `Account`).
3. Create a value of `AuthHandler Request Account` which encapsulates the
authentication logic (`Request -> Handler Account`). This function
will be executed every time a request matches a protected route.
2. choose a application-specific data type used by your server when
authentication is successful (in our case this was `User`).
3. Create a value of `AuthHandler Request User` which encapsulates the
authentication logic (`Request -> ExceptT IO ServantErr User`). This function
will be executed everytime a request matches a protected route.
4. Provide an instance of the `AuthServerData` type family, specifying your
application-specific data type returned when authentication is successful (in
our case this was `Account`).
our case this was `User`).
Caveats:
@ -408,8 +400,6 @@ As of `0.5`, *servant-client* comes with support for basic authentication!
Endpoints protected by Basic Authentication will require a value of type
`BasicAuthData` to complete the request.
You can find more comprehensive Basic Authentication example in the [Cookbook](../cookbook/basic-auth/BasicAuth.html).
### Generalized Authentication
Servant `0.5` also shipped with support for generalized authentication. Similar
@ -419,15 +409,13 @@ marshal an unauthenticated request into an authenticated request. Generally,
this will look like:
```haskell ignore
import Servant.Common.Req (Req, addHeader)
-- | The datatype we'll use to authenticate a request. If we were wrapping
-- something like OAuth, this might be a Bearer token.
type instance AuthClientData (AuthProtect "cookie-auth") = String
-- | A method to authenticate a request
authenticateReq :: String -> Req -> Req
authenticateReq s req = addHeader "my-bespoke-header" s req
authenticateReq s req = SCR.addHeader "my-bespoke-header" s req
```
Now, if the client method for our protected endpoint was `getProtected`, then

View file

@ -1,11 +1,12 @@
# Querying an API
While defining handlers that [serve an API](Server.html) has a lot to it, querying an API is simpler: we do not care about what happens inside the webserver, we just need to know how to talk to it and get a response back. That said, we usually have to write the querying functions by hand because the structure of the API isn't a first class citizen and can't be inspected to generate the client-side functions.
While defining handlers that serve an API has a lot to it, querying an API is simpler: we do not care about what happens inside the webserver, we just need to know how to talk to it and get a response back. Except that we usually have to write the querying functions by hand because the structure of the API isn't a first class citizen and can't be inspected to generate a bunch of client-side functions.
**servant** however has a way to inspect APIs, because APIs are just Haskell types and (GHC) Haskell lets us do quite a few things with types. In the same way that we look at an API type to deduce the types the handlers should have, we can inspect the structure of the API to *derive* Haskell functions that take one argument for each occurrence of `Capture`, `ReqBody`, `QueryParam`
and friends (see [the tutorial introduction](ApiType.html) for an overview). By *derive*, we mean that there's no code generation involved - the functions are defined just by the structure of the API type.
**servant** however has a way to inspect APIs, because APIs are just Haskell types and (GHC) Haskell lets us do quite a few things with types. In the same way that we look at an API type to deduce the types the handlers should have, we can inspect the structure of the API to *derive* Haskell functions that take one argument for each occurence of `Capture`, `ReqBody`, `QueryParam`
and friends. By *derive*, we mean that there's no code generation involved, the functions are defined just by the structure of the API type.
The source for this tutorial section is a literate Haskell file, so first we need to have some language extensions and imports:
The source for this tutorial section is a literate haskell file, so first we
need to have some language extensions and imports:
``` haskell
{-# LANGUAGE DataKinds #-}
@ -14,23 +15,21 @@ The source for this tutorial section is a literate Haskell file, so first we nee
module Client where
import Control.Monad.Trans.Except (ExceptT, runExceptT)
import Data.Aeson
import Data.Proxy
import GHC.Generics
import Network.HTTP.Client (newManager, defaultManagerSettings)
import Network.HTTP.Client (Manager, newManager, defaultManagerSettings)
import Servant.API
import Servant.Client
import Servant.Types.SourceT (foreach)
import qualified Servant.Client.Streaming as S
```
Also, we need examples for some domain specific data types:
``` haskell
data Position = Position
{ xCoord :: Int
, yCoord :: Int
{ x :: Int
, y :: Int
} deriving (Show, Generic)
instance FromJSON Position
@ -67,18 +66,24 @@ type API = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Posit
:<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email
```
What we are going to get with **servant-client** here is three functions, one to query each endpoint:
What we are going to get with **servant-client** here is 3 functions, one to query each endpoint:
``` haskell
position :: Int -- ^ value for "x"
-> Int -- ^ value for "y"
-> ClientM Position
-> Manager -- ^ the HTTP client to use
-> BaseUrl -- ^ the URL at which the API can be found
-> ExceptT ServantError IO Position
hello :: Maybe String -- ^ an optional value for "name"
-> ClientM HelloMessage
-> Manager -- ^ the HTTP client to use
-> BaseUrl -- ^ the URL at which the API can be found
-> ExceptT ServantError IO HelloMessage
marketing :: ClientInfo -- ^ value for the request body
-> ClientM Email
-> Manager -- ^ the HTTP client to use
-> BaseUrl -- ^ the URL at which the API can be found
-> ExceptT ServantError IO Email
```
Each function makes available as an argument any value that the response may
@ -94,20 +99,7 @@ api = Proxy
position :<|> hello :<|> marketing = client api
```
`client api` returns client functions for our _entire_ API, combined with `:<|>`, which we can pattern match on as above. You could say `client` "calculates" the correct type and number of client functions for the API type it is given (via a `Proxy`), as well as their implementations.
If you have an `EmptyAPI` in your API, servant-client will hand you a value of
type `EmptyClient` in the corresponding slot, where `data EmptyClient =
EmptyClient`, as a way to indicate that you can't do anything useful with it.
``` haskell ignore
type API' = API :<|> EmptyAPI
api' :: Proxy API'
api' = Proxy
(position' :<|> hello' :<|> marketing') :<|> EmptyClient = client api'
```
As you can see in the code above, we just "pattern match our way" to these functions. If we try to derive less or more functions than there are endpoints in the API, we obviously get an error. The `BaseUrl` value there is just:
``` haskell ignore
-- | URI scheme to use
@ -122,24 +114,23 @@ data BaseUrl = BaseUrl
{ baseUrlScheme :: Scheme -- ^ URI scheme to use
, baseUrlHost :: String -- ^ host (eg "haskell.org")
, baseUrlPort :: Int -- ^ port (eg 80)
, baseUrlPath :: String -- ^ path (eg "/a/b/c")
}
```
That's it. Let's now write some code that uses our client functions.
``` haskell
queries :: ClientM (Position, HelloMessage, Email)
queries = do
pos <- position 10 10
message <- hello (Just "servant")
em <- marketing (ClientInfo "Alp" "alp@foo.com" 26 ["haskell", "mathematics"])
queries :: Manager -> BaseUrl -> ExceptT ServantError IO (Position, HelloMessage, Email)
queries manager baseurl = do
pos <- position 10 10 manager baseurl
message <- hello (Just "servant") manager baseurl
em <- marketing (ClientInfo "Alp" "alp@foo.com" 26 ["haskell", "mathematics"]) manager baseurl
return (pos, message, em)
run :: IO ()
run = do
manager' <- newManager defaultManagerSettings
res <- runClientM queries (mkClientEnv manager' (BaseUrl Http "localhost" 8081 ""))
manager <- newManager defaultManagerSettings
res <- runExceptT (queries manager (BaseUrl Http "localhost" 8081 ""))
case res of
Left err -> putStrLn $ "Error: " ++ show err
Right (pos, message, em) -> do
@ -150,108 +141,10 @@ run = do
Here's the output of the above code running against the appropriate server:
```
Position {xCoord = 10, yCoord = 10}
``` bash
Position {x = 10, y = 10}
HelloMessage {msg = "Hello, servant"}
Email {from = "great@company.com", to = "alp@foo.com", subject = "Hey Alp, we miss you!", body = "Hi Alp,\n\nSince you've recently turned 26, have you checked out our latest haskell, mathematics products? Give us a visit!"}
```
The types of the arguments for the functions are the same as for (server-side) request handlers.
## Changing the monad the client functions live in
Just like `hoistServer` allows us to change the monad in which request handlers
of a web application live, we also have `hoistClient` for changing the monad
in which _client functions_ live. Consider the following trivial API:
``` haskell
type HoistClientAPI = Get '[JSON] Int :<|> Capture "n" Int :> Post '[JSON] Int
hoistClientAPI :: Proxy HoistClientAPI
hoistClientAPI = Proxy
```
We already know how to derive client functions for this API, and as we have
seen above they all return results in the `ClientM` monad when using `servant-client`.
However, `ClientM` is rarely (or never) the actual monad we need to use the client
functions in. Sometimes we need to run them in IO, sometimes in a custom monad
stack. `hoistClient` is a very simple solution to the problem of "changing" the monad
the clients run in.
``` haskell ignore
hoistClient
:: HasClient ClientM api -- we need a valid API
=> Proxy api -- a Proxy to the API type
-> (forall a. m a -> n a) -- a "monad conversion function" (natural transformation)
-> Client m api -- clients in the source monad
-> Client n api -- result: clients in the target monad
```
The "conversion function" argument above, just like the ones given to `hoistServer`, must
be able to turn an `m a` into an `n a` for any choice of type `a`.
Let's see this in action on our example. We first derive our client functions as usual,
with all of them returning a result in `ClientM`.
``` haskell
getIntClientM :: ClientM Int
postIntClientM :: Int -> ClientM Int
getIntClientM :<|> postIntClientM = client hoistClientAPI
```
And we finally decide that we want the handlers to run in IO instead, by
"post-applying" `runClientM` to a fixed client environment.
``` haskell
-- our conversion function has type: forall a. ClientM a -> IO a
-- the result has type:
-- Client IO HoistClientAPI = IO Int :<|> (Int -> IO Int)
getClients :: ClientEnv -> Client IO HoistClientAPI
getClients clientEnv
= hoistClient hoistClientAPI
( fmap (either (error . show) id)
. flip runClientM clientEnv
)
(client hoistClientAPI)
```
## Querying Streaming APIs.
Consider the following streaming API type:
``` haskell
type StreamAPI = "positionStream" :> StreamGet NewlineFraming JSON (SourceIO Position)
```
Note that we use the same `SourceIO` type as on the server-side
(this is different from `servant-0.14`).
However, we have to use different client, `Servant.Client.Streaming`,
which can stream (but has different API).
In any case, here's how we write a function to query our API:
```haskell
streamAPI :: Proxy StreamAPI
streamAPI = Proxy
posStream :: S.ClientM (SourceIO Position)
posStream = S.client streamAPI
```
We'll get back a `SourceIO Position`. Instead of `runClientM`, the streaming
client provides `withClientM`: the underlying HTTP connection is open only
until the inner functions exits. Inside the block we can e.g just print out
all elements from a `SourceIO`, to give some idea of how to work with them.
``` haskell
printSourceIO :: Show a => ClientEnv -> S.ClientM (SourceIO a) -> IO ()
printSourceIO env c = S.withClientM c env $ \e -> case e of
Left err -> putStrLn $ "Error: " ++ show err
Right rs -> foreach fail print rs
```
The stream is parsed and provided incrementally. So the above loop prints out
each result as soon as it is received on the stream, rather than waiting until
they are all available to print them at once.
You now know how to use **servant-client**!
The types of the arguments for the functions are the same as for (server-side) request handlers. You now know how to use **servant-client**!

View file

@ -23,7 +23,6 @@ import Network.Wai
import Servant.API
import Servant.Docs
import Servant.Server
import Web.FormUrlEncoded(FromForm(..), ToForm(..))
```
And we'll import some things from one of our earlier modules
@ -77,7 +76,7 @@ instance ToSample HelloMessage where
[ ("When a value is provided for 'name'", HelloMessage "Hello, Alp")
, ("When 'name' is not specified", HelloMessage "Hello, anonymous coward")
]
-- multiple examples to display this time
-- mutliple examples to display this time
ci :: ClientInfo
ci = ClientInfo "Alp" "alp@foo.com" 26 ["haskell", "mathematics"]
@ -90,8 +89,6 @@ instance ToSample Email where
```
Types that are used as request or response bodies have to instantiate the `ToSample` typeclass which lets you specify one or more examples of values. `Capture`s and `QueryParam`s have to instantiate their respective `ToCapture` and `ToParam` classes and provide a name and some information about the concrete meaning of that argument, as illustrated in the code above.
The `EmptyAPI` combinator needs no special treatment as it generates no
documentation: an empty API has no endpoints to document.
With all of this, we can derive docs for our API.
@ -108,9 +105,15 @@ apiDocs = docs exampleAPI
markdown :: API -> String
```
That lets us see what our API docs look like in markdown, by looking at `markdown apiDocs`.
That lets us see what our API docs look down in markdown, by looking at `markdown apiDocs`.
````````` text
## Welcome
This is our super webservice's API.
Enjoy!
## GET /hello
#### GET Parameters:
@ -127,20 +130,19 @@ That lets us see what our API docs look like in markdown, by looking at `markdow
- Supported content types are:
- `application/json;charset=utf-8`
- `application/json`
- When a value is provided for 'name' (`application/json;charset=utf-8`, `application/json`):
- When a value is provided for 'name'
```javascript
{"msg":"Hello, Alp"}
```
```javascript
{"msg":"Hello, Alp"}
```
- When 'name' is not specified (`application/json;charset=utf-8`, `application/json`):
- When 'name' is not specified
```javascript
{"msg":"Hello, anonymous coward"}
```
```javascript
{"msg":"Hello, anonymous coward"}
```
## POST /marketing
@ -148,30 +150,28 @@ That lets us see what our API docs look like in markdown, by looking at `markdow
- Supported content types are:
- `application/json;charset=utf-8`
- `application/json`
- Example (`application/json;charset=utf-8`, `application/json`):
- Example: `application/json`
```javascript
{"clientAge":26,"clientEmail":"alp@foo.com","clientName":"Alp","clientInterestedIn":["haskell","mathematics"]}
```
```javascript
{"email":"alp@foo.com","interested_in":["haskell","mathematics"],"age":26,"name":"Alp"}
```
#### Response:
- Status code 200
- Status code 201
- Headers: []
- Supported content types are:
- `application/json;charset=utf-8`
- `application/json`
- Example (`application/json;charset=utf-8`, `application/json`):
- Response body as below.
```javascript
{"subject":"Hey Alp, we miss you!","body":"Hi Alp,\n\nSince you've recently turned 26, have you checked out our latest haskell, mathematics products? Give us a visit!","to":"alp@foo.com","from":"great@company.com"}
```
```javascript
{"subject":"Hey Alp, we miss you!","body":"Hi Alp,\n\nSince you've recently turned 26, have you checked out our latest haskell, mathematics products? Give us a visit!","to":"alp@foo.com","from":"great@company.com"}
```
## GET /position/:x/:y
@ -187,14 +187,13 @@ That lets us see what our API docs look like in markdown, by looking at `markdow
- Supported content types are:
- `application/json;charset=utf-8`
- `application/json`
- Example (`application/json;charset=utf-8`, `application/json`):
- Response body as below.
```javascript
{"yCoord":14,"xCoord":3}
````
```javascript
{"x":3,"y":14}
```
`````````
@ -212,49 +211,6 @@ docsBS = encodeUtf8
`docsWithIntros` just takes an additional parameter, a list of `DocIntro`s that must be displayed before any endpoint docs.
More customisation can be done with the `markdownWith` function, which allows customising some of the parameters used when generating Markdown. The most obvious of these is how to handle when a request or response body has multiple content types. For example, if we make a slight change to the `/marketing` endpoint of our API so that the request body can also be encoded as a form:
``` haskell
type ExampleAPI2 = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position
:<|> "hello" :> QueryParam "name" String :> Get '[JSON] HelloMessage
:<|> "marketing" :> ReqBody '[JSON, FormUrlEncoded] ClientInfo :> Post '[JSON] Email
instance ToForm ClientInfo
instance FromForm ClientInfo
exampleAPI2 :: Proxy ExampleAPI2
exampleAPI2 = Proxy
api2Docs :: API
api2Docs = docs exampleAPI2
```
The relevant output of `markdown api2Docs` is now:
```````` text
#### Request:
- Supported content types are:
- `application/json;charset=utf-8`
- `application/json`
- `application/x-www-form-urlencoded`
- Example (`application/json;charset=utf-8`, `application/json`):
```javascript
{"clientAge":26,"clientEmail":"alp@foo.com","clientName":"Alp","clientInterestedIn":["haskell","mathematics"]}
```
- Example (`application/x-www-form-urlencoded`):
```
clientAge=26&clientEmail=alp%40foo.com&clientName=Alp&clientInterestedIn=haskell&clientInterestedIn=mathematics
```
````````
If, however, you don't want the extra example encoding shown, then you can use `markdownWith (defRenderingOptions & requestExamples .~ FirstContentType)` to get behaviour identical to `markdown apiDocs`.
We can now serve the API *and* the API docs with a simple server.
``` haskell
@ -264,10 +220,12 @@ api :: Proxy DocsAPI
api = Proxy
server :: Server DocsAPI
server = Server.server3 :<|> Tagged serveDocs where
serveDocs _ respond =
respond $ responseLBS ok200 [plain] docsBS
plain = ("Content-Type", "text/plain")
server = Server.server3 :<|> serveDocs
where serveDocs _ respond =
respond $ responseLBS ok200 [plain] docsBS
plain = ("Content-Type", "text/plain")
app :: Application
app = serve api server

View file

@ -136,7 +136,7 @@ server = randomPoint
server' :: Server API'
server' = server
:<|> serveDirectoryFileServer "static"
:<|> serveDirectory "static"
app :: Application
app = serve api' server'
@ -149,125 +149,23 @@ Why two different API types, proxies and servers though? Simply because we
don't want to generate javascript functions for the `Raw` part of our API type,
so we need a `Proxy` for our API type `API'` without its `Raw` endpoint.
The `EmptyAPI` combinator needs no special treatment as it generates no
Javascript functions: an empty API has no endpoints to access.
Very similarly to how one can derive haskell functions, we can derive the
javascript with just a simple function call to `jsForAPI` from
`Servant.JS`.
`Servant.JQuery`.
``` haskell
apiJS1 :: Text
apiJS1 = jsForAPI api jquery
apiJS :: Text
apiJS = jsForAPI api vanillaJS
```
This `Text` contains 2 Javascript functions, 'getPoint' and 'getBooks':
``` javascript
var getPoint = function(onSuccess, onError)
{
$.ajax(
{ url: '/point'
, success: onSuccess
, error: onError
, type: 'GET'
});
}
var getBooks = function(q, onSuccess, onError)
{
$.ajax(
{ url: '/books' + '?q=' + encodeURIComponent(q)
, success: onSuccess
, error: onError
, type: 'GET'
});
}
```
We created a directory `static` that contains two static files: `index.html`,
which is the entrypoint to our little web application; and `ui.js`, which
contains some hand-written javascript. This javascript code assumes the two
generated functions `getPoint` and `getBooks` in scope. Therefore we need to
write the generated javascript into a file:
``` haskell
writeJSFiles :: IO ()
writeJSFiles = do
T.writeFile "static/api.js" apiJS1
jq <- T.readFile =<< Language.Javascript.JQuery.file
T.writeFile "static/jq.js" jq
```
(We're also writing the jquery library into a file, as it's also used by
`ui.js`.) `static/api.js` will be included in `index.html` and the two
generated functions will therefore be available in `ui.js`.
And we're good to go. You can start the `main` function of this file and go to
`http://localhost:8000/`. Start typing in the name of one of the authors in our
database or part of a book title, and check out how long it takes to
approximate pi using the method mentioned above.
## Customizations
Instead of calling `jquery`, you can call its variant `jqueryWith`.
Here are the type definitions
```haskell ignore
jquery :: JavaScriptGenerator
jqueryWith :: CommonGeneratorOptions -> JavaScriptGenerator
```
The `CommonGeneratorOptions` will let you define different behaviors to
change how functions are generated. Here is the definition of currently
available options:
```haskell ignore
data CommonGeneratorOptions = CommonGeneratorOptions
{
-- | function generating function names
functionNameBuilder :: FunctionName -> Text
-- | name used when a user wants to send the request body (to let you redefine it)
, requestBody :: Text
-- | name of the callback parameter when the request was successful
, successCallback :: Text
-- | name of the callback parameter when the request reported an error
, errorCallback :: Text
-- | namespace on which we define the js function (empty means local var)
, moduleName :: Text
-- | a prefix that should be prepended to the URL in the generated JS
, urlPrefix :: Text
}
```
This pattern is available with all supported backends, and default values are provided.
## Vanilla support
If you don't use JQuery for your application, you can reduce your
dependencies to simply use the `XMLHttpRequest` object from the standard API.
Use the same code as before but simply replace the previous `apiJS` with
the following one:
``` haskell
apiJS2 :: Text
apiJS2 = jsForAPI api vanillaJS
```
The rest is *completely* unchanged.
The output file is a bit different, but it has the same parameters,
``` javascript
var getPoint = function(onSuccess, onError)
{
var xhr = new XMLHttpRequest();
xhr.open('GET', '/point', true);
xhr.setRequestHeader(\"Accept\",\"application/json\");
xhr.setRequestHeader("Accept","application/json");
xhr.onreadystatechange = function (e) {
if (xhr.readyState == 4) {
if (xhr.status == 204 || xhr.status == 205) {
@ -288,7 +186,7 @@ var getBooks = function(q, onSuccess, onError)
{
var xhr = new XMLHttpRequest();
xhr.open('GET', '/books' + '?q=' + encodeURIComponent(q), true);
xhr.setRequestHeader(\"Accept\",\"application/json\");
xhr.setRequestHeader("Accept","application/json");
xhr.onreadystatechange = function (e) {
if (xhr.readyState == 4) {
if (xhr.status == 204 || xhr.status == 205) {
@ -304,217 +202,27 @@ var getBooks = function(q, onSuccess, onError)
}
xhr.send(null);
}
```
And that's all, your web service can of course be accessible from those
two clients at the same time!
## Axios support
### Simple usage
If you use Axios library for your application, we support that too!
Use the same code as before but simply replace the previous `apiJS` with
the following one:
We created a directory `static` that contains two static files: `index.html`,
which is the entrypoint to our little web application; and `ui.js`, which
contains some hand-written javascript. This javascript code assumes the two
generated functions `getPoint` and `getBooks` in scope. Therefore we need to
write the generated javascript into a file:
``` haskell
apiJS3 :: Text
apiJS3 = jsForAPI api $ axios defAxiosOptions
writeJSFiles :: IO ()
writeJSFiles = do
T.writeFile "static/api.js" apiJS
jq <- T.readFile =<< Language.Javascript.JQuery.file
T.writeFile "static/jq.js" jq
```
The rest is *completely* unchanged.
(We're also writing the jquery library into a file, as it's also used by
`ui.js`.) `static/api.js` will be included in `index.html` and the two
generated functions will therefore be available in `ui.js`.
The output file is a bit different,
``` javascript
var getPoint = function()
{
return axios({ url: '/point'
, method: 'get'
});
}
var getBooks = function(q)
{
return axios({ url: '/books' + '?q=' + encodeURIComponent(q)
, method: 'get'
});
}
```
**Caution:** In order to support the promise style of the API, there are no onSuccess
nor onError callback functions.
### Defining Axios configuration
Axios lets you define a 'configuration' to determine the behavior of the
program when the AJAX request is sent.
We mapped this into a configuration
``` haskell
data AxiosOptions = AxiosOptions
{ -- | indicates whether or not cross-site Access-Control requests
-- should be made using credentials
withCredentials :: !Bool
-- | the name of the cookie to use as a value for xsrf token
, xsrfCookieName :: !(Maybe Text)
-- | the name of the header to use as a value for xsrf token
, xsrfHeaderName :: !(Maybe Text)
}
```
## Angular support
### Simple usage
You can apply the same procedure as with `vanillaJS` and `jquery`, and
generate top level functions.
The difference is that `angular` Generator always takes an argument.
``` haskell
apiJS4 :: Text
apiJS4 = jsForAPI api $ angular defAngularOptions
```
The generated code will be a bit different than previous generators. An extra
argument `$http` will be added to let Angular magical Dependency Injector
operate.
**Caution:** In order to support the promise style of the API, there are no onSuccess
nor onError callback functions.
``` javascript
var getPoint = function($http)
{
return $http(
{ url: '/point'
, method: 'GET'
});
}
var getBooks = function($http, q)
{
return $http(
{ url: '/books' + '?q=' + encodeURIComponent(q)
, method: 'GET'
});
}
```
You can then build your controllers easily
``` javascript
app.controller("MyController", function($http) {
this.getPoint = getPoint($http)
.success(/* Do something */)
.error(/* Report error */);
this.getPoint = getBooks($http, q)
.success(/* Do something */)
.error(/* Report error */);
});
```
### Service generator
You can also generate automatically a service to wrap the whole API as
a single Angular service:
``` javascript
app.service('MyService', function($http) {
return ({
postCounter: function()
{
return $http(
{ url: '/counter'
, method: 'POST'
});
},
getCounter: function()
{
return $http(
{ url: '/books' + '?q=' + encodeURIComponent(q), true);
, method: 'GET'
});
}
});
});
```
To do so, you just have to use an alternate generator.
``` haskell
apiJS5 :: Text
apiJS5 = jsForAPI api $ angularService defAngularOptions
```
Again, it is possible to customize some portions with the options.
``` haskell
data AngularOptions = AngularOptions
{ -- | When generating code with wrapInService, name of the service to generate, default is 'app'
serviceName :: Text
, -- | beginning of the service definition
prologue :: Text -> Text -> Text
, -- | end of the service definition
epilogue :: Text
}
```
## Custom function name builder
Servant comes with three name builders included:
- camelCase (the default)
- concatCase
- snakeCase
Keeping the JQuery as an example, let's see the impact:
``` haskell
apiJS6 :: Text
apiJS6 = jsForAPI api $ jqueryWith defCommonGeneratorOptions { functionNameBuilder= snakeCase }
```
This `Text` contains 2 Javascript functions:
``` javascript
var get_point = function(onSuccess, onError)
{
$.ajax(
{ url: '/point'
, success: onSuccess
, error: onError
, type: 'GET'
});
}
var get_books = function(q, onSuccess, onError)
{
$.ajax(
{ url: '/books' + '?q=' + encodeURIComponent(q)
, success: onSuccess
, error: onError
, type: 'GET'
});
}
```
And we're good to go. You can start the `main` function of this file and go to
`http://localhost:8000/`. Start typing in the name of one of the authors in our
database or part of a book title, and check out how long it takes to
approximate pi using the method mentioned above.

View file

@ -18,7 +18,6 @@ need to have some language extensions and imports:
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}
@ -29,7 +28,7 @@ import Prelude.Compat
import Control.Monad.Except
import Control.Monad.Reader
import Data.Aeson
import Data.Aeson.Compat
import Data.Aeson.Types
import Data.Attoparsec.ByteString
import Data.ByteString (ByteString)
@ -46,7 +45,6 @@ import Servant
import System.Directory
import Text.Blaze
import Text.Blaze.Html.Renderer.Utf8
import Servant.Types.SourceT (source)
import qualified Data.Aeson.Parser
import qualified Text.Blaze.Html
```
@ -96,6 +94,12 @@ users1 =
]
```
Let's also write our API type.
``` haskell ignore
type UserAPI1 = "users" :> Get '[JSON] [User]
```
We can now take care of writing the actual webservice that will handle requests
to such an API. This one will be very simple, being reduced to just a single
endpoint. The type of the web application is determined by the API type,
@ -107,11 +111,11 @@ corresponding API type.
The first thing to know about the `Server` type family is that behind the
scenes it will drive the routing, letting you focus only on the business
logic. The second thing to know is that for each endpoint, your handlers will
by default run in the `Handler` monad. This is overridable very
by default run in the `ExceptT ServantErr IO` monad. This is overridable very
easily, as explained near the end of this guide. Third thing, the type of the
value returned in that monad must be the same as the second argument of the
HTTP method combinator used for the corresponding endpoint. In our case, it
means we must provide a handler of type `Handler [User]`. Well,
means we must provide a handler of type `ExceptT ServantErr IO [User]`. Well,
we have a monad, let's just `return` our list:
``` haskell
@ -183,7 +187,7 @@ users2 = [isaac, albert]
Now, just like we separate the various endpoints in `UserAPI` with `:<|>`, we
are going to separate the handlers with `:<|>` too! They must be provided in
the same order as in the API type.
the same order as in in the API type.
``` haskell
server2 :: Server UserAPI2
@ -199,7 +203,7 @@ And that's it! You can run this example in the same way that we showed for
Fine, we can write trivial webservices easily, but none of the two above use
any "fancy" combinator from servant. Let's address this and use `QueryParam`,
`Capture` and `ReqBody` right away. You'll see how each occurrence of these
`Capture` and `ReqBody` right away. You'll see how each occurence of these
combinators in an endpoint makes the corresponding handler receive an
argument of the appropriate type automatically. You don't have to worry about
manually looking up URL captures or query string parameters, or
@ -265,15 +269,15 @@ server3 = position
:<|> hello
:<|> marketing
where position :: Int -> Int -> Handler Position
where position :: Int -> Int -> ExceptT ServantErr IO Position
position x y = return (Position x y)
hello :: Maybe String -> Handler HelloMessage
hello :: Maybe String -> ExceptT ServantErr IO HelloMessage
hello mname = return . HelloMessage $ case mname of
Nothing -> "Hello, anonymous coward"
Just n -> "Hello, " ++ n
marketing :: ClientInfo -> Handler Email
marketing :: ClientInfo -> ExceptT ServantErr IO Email
marketing clientinfo = return (emailForClient clientinfo)
```
@ -303,7 +307,7 @@ $ curl -X POST -d '{"clientName":"Alp Mestanogullari", "clientEmail" : "alp@foo.
For reference, here's a list of some combinators from **servant**:
> - `Delete`, `Get`, `Patch`, `Post`, `Put`: these do not become arguments. They provide the return type of handlers, which usually is `Handler <something>`.
> - `Delete`, `Get`, `Patch`, `Post`, `Put`: these do not become arguments. They provide the return type of handlers, which usually is `ExceptT ServantErr IO <something>`.
> - `Capture "something" a` becomes an argument of type `a`.
> - `QueryParam "something" a`, `Header "something" a` all become arguments of type `Maybe a`, because there might be no value at all specified by the client for these.
> - `QueryFlag "something"` gets turned into an argument of type `Bool`.
@ -313,8 +317,8 @@ For reference, here's a list of some combinators from **servant**:
## The `FromHttpApiData`/`ToHttpApiData` classes
Wait... How does **servant** know how to decode the `Int`s from the URL? Or how
to decode a `ClientInfo` value from the request body? The following three sections will
help us answer these questions.
to decode a `ClientInfo` value from the request body? This is what this and the
following two sections address.
`Capture`s and `QueryParam`s are represented by some textual value in URLs.
`Header`s are similarly represented by a pair of a header name and a
@ -597,9 +601,10 @@ $ curl -H 'Accept: text/html' http://localhost:8081/persons
# or just point your browser to http://localhost:8081/persons
```
## The `Handler` monad
## The `ExceptT ServantErr IO` monad
At the heart of the handlers is the monad they run in, namely a newtype `Handler` around `ExceptT ServerError IO`
At the heart of the handlers is the monad they run in, namely `ExceptT
ServantErr IO`
([haddock documentation for `ExceptT`](http://hackage.haskell.org/package/mtl-2.2.1/docs/Control-Monad-Except.html#t:ExceptT)).
One might wonder: why this monad? The answer is that it is the
simplest monad with the following properties:
@ -616,13 +621,13 @@ Let's recall some definitions.
newtype ExceptT e m a = ExceptT (m (Either e a))
```
In short, this means that a handler of type `Handler a` is simply
equivalent to a computation of type `IO (Either ServerError a)`, that is, an IO
In short, this means that a handler of type `ExceptT ServantErr IO a` is simply
equivalent to a computation of type `IO (Either ServantErr a)`, that is, an IO
action that either returns an error or a result.
The module [`Control.Monad.Except`](https://hackage.haskell.org/package/mtl/docs/Control-Monad-Except.html#t:ExceptT)
The module [`Control.Monad.Except`](https://hackage.haskell.org/package/mtl-2.2.1/docs/Control-Monad-Except.html#t:ExceptT)
from which `ExceptT` comes is worth looking at.
Perhaps most importantly, `ExceptT` and `Handler` are instances of `MonadError`, so
Perhaps most importantly, `ExceptT` is an instance of `MonadError`, so
`throwError` can be used to return an error from your handler (whereas `return`
is enough to return a success).
@ -632,9 +637,9 @@ kind and abort early. The next two sections cover how to do just that.
### Performing IO
Other important instances from the list above are `MonadIO m => MonadIO
(ExceptT e m)`, and therefore also `MonadIO Handler` as there is a `MonadIO IO` instance.
[`MonadIO`](http://hackage.haskell.org/package/base/docs/Control-Monad-IO-Class.html#t:MonadIO)
Another important instance from the list above is `MonadIO m => MonadIO
(ExceptT e m)`.
[`MonadIO`](http://hackage.haskell.org/package/transformers-0.4.3.0/docs/Control-Monad-IO-Class.html)
is a class from the **transformers** package defined as:
``` haskell ignore
@ -642,7 +647,8 @@ class Monad m => MonadIO m where
liftIO :: IO a -> m a
```
So if you want to run any kind of
The `IO` monad provides a `MonadIO` instance. Hence for any type
`e`, `ExceptT e IO` has a `MonadIO` instance. So if you want to run any kind of
IO computation in your handlers, just use `liftIO`:
``` haskell
@ -660,16 +666,16 @@ server5 = do
return (FileContent filecontent)
```
### Failing, through `ServerError`
### Failing, through `ServantErr`
If you want to explicitly fail at providing the result promised by an endpoint
using the appropriate HTTP status code (not found, unauthorized, etc) and some
error message, all you have to do is use the `throwError` function mentioned above
and provide it with the appropriate value of type `ServerError`, which is
and provide it with the appropriate value of type `ServantErr`, which is
defined as:
``` haskell ignore
data ServerError = ServerError
data ServantErr = ServantErr
{ errHTTPCode :: Int
, errReasonPhrase :: String
, errBody :: ByteString -- lazy bytestring
@ -682,10 +688,10 @@ module. If you want to use these values but add a body or some headers, just
use record update syntax:
``` haskell
failingHandler :: Handler ()
failingHandler :: ExceptT ServantErr IO ()
failingHandler = throwError myerr
where myerr :: ServerError
where myerr :: ServantErr
myerr = err503 { errBody = "Sorry dear user." }
```
@ -716,7 +722,7 @@ $ curl --verbose http://localhost:8081/myfile.txt
>
< HTTP/1.1 404 Not Found
[snip]
myfile.txt just isn't there, please leave this server alone.
myfile.txt just isnt there, please leave this server alone.
$ echo Hello > myfile.txt
@ -748,29 +754,7 @@ myHandler :: Server MyHandler
myHandler = return $ addHeader 1797 albert
```
Note that the type of `addHeader header x` is different than the type of `x`!
And if you add more headers, more headers will appear in the header list:
``` haskell
type MyHeadfulHandler = Get '[JSON] (Headers '[Header "X-A-Bool" Bool, Header "X-An-Int" Int] User)
myHeadfulHandler :: Server MyHeadfulHandler
myHeadfulHandler = return $ addHeader True $ addHeader 1797 albert
```
But what if your handler only *sometimes* adds a header? If you declare that
your handler adds headers, and you don't add one, the return type of your
handler will be different than expected. To solve this, you have to explicitly
*not* add a header by using `noHeader`:
``` haskell
type MyMaybeHeaderHandler
= Capture "withHeader" Bool :> Get '[JSON] (Headers '[Header "X-An-Int" Int] User)
myMaybeHeaderHandler :: Server MyMaybeHeaderHandler
myMaybeHeaderHandler x = return $ if x then addHeader 1797 albert
else noHeader albert
```
Note that the type of `addHeader x` is different than the type of `x`!
## Serving static files
@ -782,10 +766,10 @@ directory serving WAI application, namely:
``` haskell ignore
-- exported by Servant and Servant.Server
serveDirectoryWebApp :: FilePath -> Server Raw
serveDirectory :: FilePath -> Server Raw
```
`serveDirectoryWebApp`'s argument must be a path to a valid directory.
`serveDirectory`'s argument must be a path to a valid directory.
Here's an example API that will serve some static files:
@ -802,7 +786,7 @@ staticAPI = Proxy
``` haskell
server7 :: Server StaticAPI
server7 = serveDirectoryWebApp "static-files"
server7 = serveDirectory "static-files"
app3 :: Application
app3 = serve staticAPI server7
@ -816,10 +800,6 @@ In other words: If a client requests `/static/foo.txt`, the server will look for
`./static-files/foo.txt`. If that file exists it'll succeed and serve the file.
If it doesn't exist, the handler will fail with a `404` status code.
`serveDirectoryWebApp` uses some standard settings that fit the use case of
serving static files for most web apps. You can find out about the other
options in the documentation of the `Servant.Server.StaticFiles` module.
## Nested APIs
Let's see how you can define APIs in a modular way, while avoiding repetition.
@ -830,7 +810,7 @@ type UserAPI3 = -- view the user with given userid, in JSON
Capture "userid" Int :> Get '[JSON] User
:<|> -- delete the user with given userid. empty response
Capture "userid" Int :> DeleteNoContent
Capture "userid" Int :> Delete '[] ()
```
We can instead factor out the `userid`:
@ -838,7 +818,7 @@ We can instead factor out the `userid`:
``` haskell
type UserAPI4 = Capture "userid" Int :>
( Get '[JSON] User
:<|> DeleteNoContent
:<|> Delete '[] ()
)
```
@ -846,26 +826,26 @@ However, you have to be aware that this has an effect on the type of the
corresponding `Server`:
``` haskell ignore
Server UserAPI3 = (Int -> Handler User)
:<|> (Int -> Handler NoContent)
Server UserAPI3 = (Int -> ExceptT ServantErr IO User)
:<|> (Int -> ExceptT ServantErr IO ())
Server UserAPI4 = Int -> ( Handler User
:<|> Handler NoContent
Server UserAPI4 = Int -> ( ExceptT ServantErr IO User
:<|> ExceptT ServantErr IO ()
)
```
In the first case, each handler receives the *userid* argument. In the latter,
the whole `Server` takes the *userid* and has handlers that are just
computations in `Handler`, with no arguments. In other words:
computations in `ExceptT`, with no arguments. In other words:
``` haskell
server8 :: Server UserAPI3
server8 = getUser :<|> deleteUser
where getUser :: Int -> Handler User
where getUser :: Int -> ExceptT ServantErr IO User
getUser _userid = error "..."
deleteUser :: Int -> Handler NoContent
deleteUser :: Int -> ExceptT ServantErr IO ()
deleteUser _userid = error "..."
-- notice how getUser and deleteUser
@ -874,10 +854,10 @@ server8 = getUser :<|> deleteUser
server9 :: Server UserAPI4
server9 userid = getUser userid :<|> deleteUser userid
where getUser :: Int -> Handler User
where getUser :: Int -> ExceptT ServantErr IO User
getUser = error "..."
deleteUser :: Int -> Handler NoContent
deleteUser :: Int -> ExceptT ServantErr IO ()
deleteUser = error "..."
```
@ -896,13 +876,13 @@ type API1 = "users" :>
-- we factor out the Request Body
type API2 = ReqBody '[JSON] User :>
( Get '[JSON] User -- just display the same user back, don't register it
:<|> PostNoContent -- register the user. empty response
:<|> Post '[JSON] () -- register the user. empty response
)
-- we factor out a Header
type API3 = Header "Authorization" Token :>
( Get '[JSON] SecretData -- get some secret data, if authorized
:<|> ReqBody '[JSON] SecretData :> PostNoContent -- add some secret data, if authorized
:<|> ReqBody '[JSON] SecretData :> Post '[] () -- add some secret data, if authorized
)
newtype Token = Token ByteString
@ -915,44 +895,44 @@ API type only at the end.
``` haskell
type UsersAPI =
Get '[JSON] [User] -- list users
:<|> ReqBody '[JSON] User :> PostNoContent -- add a user
:<|> ReqBody '[JSON] User :> Post '[] () -- add a user
:<|> Capture "userid" Int :>
( Get '[JSON] User -- view a user
:<|> ReqBody '[JSON] User :> PutNoContent -- update a user
:<|> DeleteNoContent -- delete a user
:<|> ReqBody '[JSON] User :> Put '[] () -- update a user
:<|> Delete '[] () -- delete a user
)
usersServer :: Server UsersAPI
usersServer = getUsers :<|> newUser :<|> userOperations
where getUsers :: Handler [User]
where getUsers :: ExceptT ServantErr IO [User]
getUsers = error "..."
newUser :: User -> Handler NoContent
newUser :: User -> ExceptT ServantErr IO ()
newUser = error "..."
userOperations userid =
viewUser userid :<|> updateUser userid :<|> deleteUser userid
where
viewUser :: Int -> Handler User
viewUser :: Int -> ExceptT ServantErr IO User
viewUser = error "..."
updateUser :: Int -> User -> Handler NoContent
updateUser :: Int -> User -> ExceptT ServantErr IO ()
updateUser = error "..."
deleteUser :: Int -> Handler NoContent
deleteUser :: Int -> ExceptT ServantErr IO ()
deleteUser = error "..."
```
``` haskell
type ProductsAPI =
Get '[JSON] [Product] -- list products
:<|> ReqBody '[JSON] Product :> PostNoContent -- add a product
:<|> ReqBody '[JSON] Product :> Post '[] () -- add a product
:<|> Capture "productid" Int :>
( Get '[JSON] Product -- view a product
:<|> ReqBody '[JSON] Product :> PutNoContent -- update a product
:<|> DeleteNoContent -- delete a product
:<|> ReqBody '[JSON] Product :> Put '[] () -- update a product
:<|> Delete '[] () -- delete a product
)
data Product = Product { productId :: Int }
@ -960,23 +940,23 @@ data Product = Product { productId :: Int }
productsServer :: Server ProductsAPI
productsServer = getProducts :<|> newProduct :<|> productOperations
where getProducts :: Handler [Product]
where getProducts :: ExceptT ServantErr IO [Product]
getProducts = error "..."
newProduct :: Product -> Handler NoContent
newProduct :: Product -> ExceptT ServantErr IO ()
newProduct = error "..."
productOperations productid =
viewProduct productid :<|> updateProduct productid :<|> deleteProduct productid
where
viewProduct :: Int -> Handler Product
viewProduct :: Int -> ExceptT ServantErr IO Product
viewProduct = error "..."
updateProduct :: Int -> Product -> Handler NoContent
updateProduct :: Int -> Product -> ExceptT ServantErr IO ()
updateProduct = error "..."
deleteProduct :: Int -> Handler NoContent
deleteProduct :: Int -> ExceptT ServantErr IO ()
deleteProduct = error "..."
```
@ -996,44 +976,34 @@ abstract that away:
-- indexed by values of type 'i'
type APIFor a i =
Get '[JSON] [a] -- list 'a's
:<|> ReqBody '[JSON] a :> PostNoContent -- add an 'a'
:<|> ReqBody '[JSON] a :> Post '[] () -- add an 'a'
:<|> Capture "id" i :>
( Get '[JSON] a -- view an 'a' given its "identifier" of type 'i'
:<|> ReqBody '[JSON] a :> PutNoContent -- update an 'a'
:<|> DeleteNoContent -- delete an 'a'
:<|> ReqBody '[JSON] a :> Put '[] () -- update an 'a'
:<|> Delete '[] () -- delete an 'a'
)
-- Build the appropriate 'Server'
-- given the handlers of the right type.
serverFor :: Handler [a] -- handler for listing of 'a's
-> (a -> Handler NoContent) -- handler for adding an 'a'
-> (i -> Handler a) -- handler for viewing an 'a' given its identifier of type 'i'
-> (i -> a -> Handler NoContent) -- updating an 'a' with given id
-> (i -> Handler NoContent) -- deleting an 'a' given its id
serverFor :: ExceptT ServantErr IO [a] -- handler for listing of 'a's
-> (a -> ExceptT ServantErr IO ()) -- handler for adding an 'a'
-> (i -> ExceptT ServantErr IO a) -- handler for viewing an 'a' given its identifier of type 'i'
-> (i -> a -> ExceptT ServantErr IO ()) -- updating an 'a' with given id
-> (i -> ExceptT ServantErr IO ()) -- deleting an 'a' given its id
-> Server (APIFor a i)
serverFor = error "..."
-- implementation left as an exercise. contact us on IRC
-- or the mailing list if you get stuck!
```
When your API contains the `EmptyAPI` combinator, you'll want to use
`emptyServer` in the corresponding slot for your server, which will simply fail
with 404 whenever a request reaches it:
``` haskell
type CombinedAPI2 = API :<|> "empty" :> EmptyAPI
server11 :: Server CombinedAPI2
server11 = server3 :<|> emptyServer
```
## Using another monad for your handlers
Remember how `Server` turns combinators for HTTP methods into `Handler`? Well, actually, there's more to that. `Server` is actually a
Remember how `Server` turns combinators for HTTP methods into `ExceptT
ServantErr IO`? Well, actually, there's more to that. `Server` is actually a
simple type synonym.
``` haskell ignore
type Server api = ServerT api Handler
type Server api = ServerT api (ExceptT ServantErr IO)
```
`ServerT` is the actual type family that computes the required types for the
@ -1053,71 +1023,77 @@ into something **servant** can understand?
If we have a function that gets us from an `m a` to an `n a`, for any `a`, what
do we have?
``` haskell
type (~>) m n = forall a. m a -> n a
``` haskell ignore
newtype m :~> n = Nat { unNat :: forall a. m a -> n a}
```
For example:
``` haskell
listToMaybe' :: [] ~> Maybe
listToMaybe' = listToMaybe -- from Data.Maybe
listToMaybeNat :: [] :~> Maybe
listToMaybeNat = Nat listToMaybe -- from Data.Maybe
```
Note that `servant` doesn't declare the `~>` type-alias, as the unfolded
variant isn't much longer to write, as we'll see shortly.
(`Nat` comes from "natural transformation", in case you're wondering.)
So if you want to write handlers using another monad/type than `Handler`, say the `Reader String` monad, the first thing you have to
So if you want to write handlers using another monad/type than `ExceptT
ServantErr IO`, say the `Reader String` monad, the first thing you have to
prepare is a function:
``` haskell ignore
readerToHandler :: Reader String a -> Handler a
readerToHandler :: Reader String :~> ExceptT ServantErr IO
```
We obviously have to run the `Reader` computation by supplying it with a
`String`, like `"hi"`. We get an `a` out from that and can then just `return`
it into `Handler`.
Let's start with `readerToHandler'`. We obviously have to run the `Reader`
computation by supplying it with a `String`, like `"hi"`. We get an `a` out
from that and can then just `return` it into `ExceptT`. We can then just wrap
that function with the `Nat` constructor to make it have the fancier type.
``` haskell
readerToHandler :: Reader String a -> Handler a
readerToHandler r = return (runReader r "hi")
readerToHandler' :: forall a. Reader String a -> ExceptT ServantErr IO a
readerToHandler' r = return (runReader r "hi")
readerToHandler :: Reader String :~> ExceptT ServantErr IO
readerToHandler = Nat readerToHandler'
```
We can write some simple webservice with the handlers running in `Reader String`.
``` haskell
type ReaderAPI = "a" :> Get '[JSON] Int
:<|> "b" :> ReqBody '[JSON] Double :> Get '[JSON] Bool
:<|> "b" :> Get '[JSON] String
readerAPI :: Proxy ReaderAPI
readerAPI = Proxy
readerServerT :: ServerT ReaderAPI (Reader String)
readerServerT = a :<|> b where
a :: Reader String Int
a = return 1797
readerServerT = a :<|> b
b :: Double -> Reader String Bool
b _ = asks (== "hi")
where a :: Reader String Int
a = return 1797
b :: Reader String String
b = ask
```
We unfortunately can't use `readerServerT` as an argument of `serve`, because
`serve` wants a `Server ReaderAPI`, i.e., with handlers running in `Handler`. But there's a simple solution to this.
`serve` wants a `Server ReaderAPI`, i.e., with handlers running in `ExceptT
ServantErr IO`. But there's a simple solution to this.
### Welcome `hoistServer`
### Enter `enter`
That's right. We have just written `readerToHandler`, which is exactly what we
would need to apply to all handlers to make the handlers have the
right type for `serve`. Being cumbersome to do by hand, we provide a function
`hoistServer` which takes a natural transformation between two parameterized types `m`
`enter` which takes a natural transformation between two parametrized types `m`
and `n` and a `ServerT someapi m`, and returns a `ServerT someapi n`.
In our case, we can wrap up our little webservice by using
`hoistServer readerAPI readerToHandler` on our handlers.
In our case, we can wrap up our little webservice by using `enter
readerToHandler` on our handlers.
``` haskell
readerServer :: Server ReaderAPI
readerServer = hoistServer readerAPI readerToHandler readerServerT
readerServer = enter readerToHandler readerServerT
app4 :: Application
app4 = serve readerAPI readerServer
@ -1128,71 +1104,10 @@ This is the webservice in action:
``` bash
$ curl http://localhost:8081/a
1797
$ curl http://localhost:8081/b -X GET -d '42.0' -H 'Content-Type: application/json'
true
$ curl http://localhost:8081/b
"hi"
```
### An arrow is a reader too.
In previous versions of `servant` we had an `enter` to do what `hoistServer`
does now. `enter` had an ambitious design goals, but was problematic in practice.
One problematic situation was when the source monad was `(->) r`, yet it's
handy in practice, because `(->) r` is isomorphic to `Reader r`.
We can rewrite the previous example without `Reader`:
```haskell
funServerT :: ServerT ReaderAPI ((->) String)
funServerT = a :<|> b where
a :: String -> Int
a _ = 1797
-- unfortunately, we cannot make `String` the first argument.
b :: Double -> String -> Bool
b _ s = s == "hi"
funToHandler :: (String -> a) -> Handler a
funToHandler f = return (f "hi")
app5 :: Application
app5 = serve readerAPI (hoistServer readerAPI funToHandler funServerT)
```
## Streaming endpoints
We can create endpoints that don't just give back a single result, but give
back a *stream* of results, served one at a time. Stream endpoints only provide
a single content type, and also specify what framing strategy is used to
delineate the results. To serve these results, we need to give back a stream
producer. Adapters can be written to *Pipes*, *Conduit* and the like, or
written directly as `SourceIO`s. SourceIO builds upon servant's own `SourceT`
stream type (it's simpler than *Pipes* or *Conduit*).
The API of a streaming endpoint needs to explicitly specify which sort of
generator it produces. Note that the generator itself is returned by a
`Handler` action, so that additional IO may be done in the creation of one.
``` haskell
type StreamAPI = "userStream" :> StreamGet NewlineFraming JSON (SourceIO User)
streamAPI :: Proxy StreamAPI
streamAPI = Proxy
streamUsers :: SourceIO User
streamUsers = source [isaac, albert, albert]
app6 :: Application
app6 = serve streamAPI (return streamUsers)
```
This simple application returns a stream of `User` values encoded in JSON
format, with each value separated by a newline. In this case, the stream will
consist of the value of `isaac`, followed by the value of `albert`, then the
value of `albert` a second time. Importantly, the stream is written back as
results are produced, rather than all at once. This means first that results
are delivered when they are available, and second, that if an exception
interrupts production of the full stream, nonetheless partial results have
already been written back.
## Conclusion
You're now equipped to write webservices/web-applications using

View file

@ -1,15 +1,21 @@
Tutorial
========
This is an introductory tutorial to **servant**. Whilst browsing is fine, it makes more sense if you read the sections in order, or at least read the first section before anything else.
This is an introductory tutorial to **servant**.
.. note::
This tutorial is for the latest version of servant. The tutorial for
servant-0.4 can be viewed
`here <https://haskell-servant.github.io/tutorial/>`_.
(Any comments, issues or feedback about the tutorial can be handled
through
`servant's issue tracker <http://github.com/haskell-servant/servant/issues>`_.)
Any comments, issues or feedback about the tutorial can be submitted
to `servant's issue tracker <http://github.com/haskell-servant/servant/issues>`_.
.. toctree::
:maxdepth: 1
install.rst
ApiType.lhs
Server.lhs
Client.lhs

View file

@ -1,68 +0,0 @@
Install
========
cabal-install
--------
The whole tutorial is a `cabal <https://cabal.readthedocs.io/en/latest/>`_
project and can be built locally as follows:
.. code-block:: bash
$ git clone https://github.com/haskell-servant/servant.git
$ cd servant
# build
$ cabal new-build tutorial
# load in ghci to play with it
$ cabal new-repl tutorial
stack
--------
The servant `stack <https://docs.haskellstack.org/en/stable/README/>`_ template includes the working tutorial. To initialize this template, run:
.. code-block:: bash
$ stack new myproj servant
$ cd myproj
# build
$ stack build
# start server
$ stack exec myproj-exe
The code can be found in the `*.lhs` files under `doc/tutorial/` in the
repository. Feel free to edit it while you're reading this documentation and
see the effect of your changes.
nix
--------
`Nix <https://nixos.org/nix/>`_ users should feel free to take a look at
the `nix/shell.nix` file in the repository and use it to provision a suitable
environment to build and run the examples.
Note for Ubuntu users
--------
Ubuntu's packages for `ghc`, `cabal`, and `stack` are years out of date.
If the instructions above fail for you,
try replacing the Ubuntu packages with up-to-date versions.
First remove the installed versions:
.. code-block:: bash
# remove the obsolete versions
$ sudo apt remove ghc haskell-stack cabal-install
Then install fresh versions of the Haskell toolchain
using the `ghcup <https://www.haskell.org/ghcup/install/>`_ installer.
As of February 2022, one easy way to do this is by running a bootstrap script:
.. code-block:: bash
$ curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
The script is interactive and will prompt you for details about what
you want installed and where. To install manually,
see `the detailed instructions <https://www.haskell.org/ghcup/install/#manual-install>`_.

View file

@ -15,10 +15,7 @@ spec = do
describe "apiJS" $ do
it "is contained verbatim in Javascript.lhs" $ do
code <- readFile "Javascript.lhs"
cs apiJS1 `shouldSatisfy` (`isInfixOf` code)
cs apiJS3 `shouldSatisfy` (`isInfixOf` code)
cs apiJS4 `shouldSatisfy` (`isInfixOf` code)
cs apiJS6 `shouldSatisfy` (`isInfixOf` code)
cs apiJS `shouldSatisfy` (`isInfixOf` code)
describe "writeJSFiles" $ do
it "[not a test] write apiJS to static/api.js" $ do
@ -27,7 +24,7 @@ spec = do
describe "app" $ with (return app) $ do
context "/api.js" $ do
it "delivers apiJS" $ do
get "/api.js" `shouldRespondWith` (fromString (cs apiJS1))
get "/api.js" `shouldRespondWith` (fromString (cs apiJS))
context "/" $ do
it "delivers something" $ do

View file

@ -1,11 +1 @@
module Main where
import qualified JavascriptSpec
import Test.Hspec (Spec, hspec, describe)
main :: IO ()
main = hspec spec
spec :: Spec
spec = describe "Javascript" JavascriptSpec.spec
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}

13
doc/tutorial/tinc.yaml Normal file
View file

@ -0,0 +1,13 @@
dependencies:
- name: servant
path: ../../servant
- name: servant-server
path: ../../servant-server
- name: servant-client
path: ../../servant-client
- name: servant-js
path: ../../servant-js
- name: servant-docs
path: ../../servant-docs
- name: servant-foreign
path: ../../servant-foreign

View file

@ -1,86 +1,64 @@
cabal-version: 2.2
name: tutorial
version: 0.10
version: 0.6.1
synopsis: The servant tutorial
description:
The servant tutorial can be found at
<http://docs.servant.dev/>
homepage: http://docs.servant.dev/
category: Servant, Documentation
license: BSD-3-Clause
homepage: http://haskell-servant.github.io/
license: BSD3
license-file: LICENSE
author: Servant Contributors
maintainer: haskell-servant-maintainers@googlegroups.com
build-type: Simple
tested-with:
GHC==8.6.5
GHC==8.8.3, GHC ==8.10.7
extra-source-files:
static/index.html
static/ui.js
cabal-version: >=1.10
library
default-language: Haskell2010
ghc-options: -Wall -pgmL markdown-unlit
exposed-modules: ApiType
, Authentication
, Client
, Docs
, Javascript
, Server
-- Packages `servant` depends on.
-- We don't need to specify bounds here as this package is never released.
build-depends:
base >= 4.7 && <5
, aeson
, attoparsec
, base-compat
, bytestring
, containers
, directory
, http-api-data
, http-client
, http-media
, http-types
, mtl
, string-conversions
, text
, transformers
, wai
, warp
-- Servant dependencies
build-depends:
servant
, servant-server
, servant-client
, servant-docs
-- Other dependencies: Lower bound around what is in the latest Stackage LTS.
-- Here can be exceptions if we really need features from the newer versions.
build-depends:
blaze-html >= 0.9.0.1 && < 0.10
, blaze-markup >= 0.8.0.0 && < 0.9
, cookie >= 0.4.3 && < 0.5
, js-jquery >= 3.3.1 && < 3.4
, lucid >= 2.9.11 && < 2.12
, random >= 1.1 && < 1.3
, servant-js >= 0.9 && < 0.10
, time >= 1.6.0.1 && < 1.13
-- For legacy tools, we need to specify build-depends too
build-depends: markdown-unlit >= 0.5.0 && <0.6
build-tool-depends: markdown-unlit:markdown-unlit >= 0.5.0 && <0.6
build-depends: base == 4.*
, base-compat
, text
, aeson
, aeson-compat
, blaze-html
, directory
, blaze-markup
, containers
, servant == 0.6.*
, servant-server == 0.6.*
, servant-client == 0.6.*
, servant-docs == 0.6.*
, servant-js == 0.6.*
, warp
, http-media
, lucid
, time
, string-conversions
, bytestring
, attoparsec
, mtl
, random
, js-jquery
, wai
, http-types
, transformers
, markdown-unlit >= 0.4
, http-client
default-language: Haskell2010
ghc-options: -Wall -Werror -pgmL markdown-unlit
-- to silence aeson-0.10 warnings:
ghc-options: -fno-warn-missing-methods
ghc-options: -fno-warn-name-shadowing
test-suite spec
type: exitcode-stdio-1.0
ghc-options: -Wall
ghc-options:
-Wall -fno-warn-name-shadowing -fno-warn-missing-signatures
default-language: Haskell2010
hs-source-dirs: test
main-is: Spec.hs
other-modules: JavascriptSpec
build-depends: base
build-depends: base == 4.*
, tutorial
, hspec
, hspec-wai

View file

@ -1,22 +0,0 @@
let reflex-platform = import (builtins.fetchTarball
{ name = "reflex-platform";
url = "https://github.com/reflex-frp/reflex-platform/archive/1aba6f367982bd6dd78ec2fda75ab246a62d32c5.tar.gz";
}) {};
pkgs = import ./nix/nixpkgs.nix; in
pkgs.stdenv.mkDerivation {
name = "ghcjs-shell";
buildInputs =
[ (reflex-platform.ghcjs.ghcWithPackages (p: with p; [
attoparsec
hashable
]))
pkgs.cabal-install
pkgs.gmp
pkgs.haskellPackages.cabal-plan
pkgs.haskellPackages.hspec-discover
pkgs.nodejs
pkgs.perl
pkgs.zlib
];
}

View file

@ -1,65 +0,0 @@
# HLint configuration file
# https://github.com/ndmitchell/hlint
##########################
# This file contains a template configuration file, which is typically
# placed as .hlint.yaml in the root of your project
# Specify additional command line arguments
#
# - arguments: [--color, --cpp-simple, -XQuasiQuotes]
# Control which extensions/flags/modules/functions can be used
#
# - extensions:
# - default: false # all extension are banned by default
# - name: [PatternGuards, ViewPatterns] # only these listed extensions can be used
# - {name: CPP, within: CrossPlatform} # CPP can only be used in a given module
#
# - flags:
# - {name: -w, within: []} # -w is allowed nowhere
#
# - modules:
# - {name: [Data.Set, Data.HashSet], as: Set} # if you import Data.Set qualified, it must be as 'Set'
# - {name: Control.Arrow, within: []} # Certain modules are banned entirely
#
# - functions:
# - {name: unsafePerformIO, within: []} # unsafePerformIO can only appear in no modules
# Add custom hints for this project
#
# Will suggest replacing "wibbleMany [myvar]" with "wibbleOne myvar"
# - error: {lhs: "wibbleMany [x]", rhs: wibbleOne x}
# Turn on hints that are off by default
#
# Ban "module X(module X) where", to require a real export list
# - warn: {name: Use explicit module export list}
#
# Replace a $ b $ c with a . b $ c
# - group: {name: dollar, enabled: true}
#
# Generalise map to fmap, ++ to <>
# - group: {name: generalise, enabled: true}
# Ignore some builtin hints
- ignore: {name: Redundant do}
- ignore: {name: Parse error}
- ignore: {name: Use fmap}
- ignore: {name: Use list comprehension}
- ignore: {name: Use lambda-case}
- ignore: {name: Eta reduce}
# - ignore: {name: Use const, within: SpecialModule} # Only within certain modules
# Define some custom infix operators
# - fixity: infixr 3 ~^#^~
# To generate a suitable file for HLint do:
# $ hlint --default > .hlint.yaml

View file

@ -1,41 +0,0 @@
You can use the `shell.nix` from this directory
to build the servant packages or even the tutorial
or cookbook if you want to, optionally.
Just the servant packages:
``` sh
$ nix-shell nix/shell.nix
```
Everything needed for the tutorial and the
cookbook too:
``` sh
$ nix-shell nix/shell.nix --arg tutorial true
```
The `shell.nix` file also supports specifying
a particular ghc version, e.g:
``` sh
$ nix-shell nix/shell.nix --argstr compiler ghcHEAD
```
**Possible GHC versions**
- `ghc865Binary`
- `ghc884`
- `ghc8104` - default
- `ghc901`
### Cabal users
GHC version can be chosen via the nix-shell parameter
`cabal build all`
### Stack version
Since the ghc version is set by the LTS version, it is preferable to use the `ghc8104` version parameter for the nix-shell.
`stack --no-nix --system-ghc <command>`

View file

@ -1,4 +0,0 @@
{
"rev" : "05f0934825c2a0750d4888c4735f9420c906b388",
"sha256" : "1g8c2w0661qn89ajp44znmwfmghbbiygvdzq0rzlvlpdiz28v6gy"
}

View file

@ -1,4 +0,0 @@
import (builtins.fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/refs/tags/21.05.tar.gz";
sha256 = "sha256:1ckzhh24mgz6jd1xhfgx0i9mijk6xjqxwsshnvq789xsavrmsc36";
}) {}

View file

@ -1,20 +0,0 @@
{ compiler ? "ghc8104"
, tutorial ? false
, pkgs ? import ./nixpkgs.nix
}:
with pkgs;
let
ghc = haskell.packages.${compiler}.ghcWithPackages (_: []);
docstuffs = python3.withPackages (ps: with ps; [ recommonmark sphinx sphinx_rtd_theme ]);
in
stdenv.mkDerivation {
name = "servant-dev";
buildInputs = [ ghc zlib python3 wget cabal-install postgresql openssl stack haskellPackages.hspec-discover ]
++ (if tutorial then [docstuffs postgresql] else []);
shellHook = ''
eval $(grep export ${ghc}/bin/ghc)
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:"${zlib}/lib";
'';
}

8
scripts/README.md Normal file
View file

@ -0,0 +1,8 @@
The release process works roughly like this:
``` bash
./scripts/bump-versions.sh <type-of-bump>
git commit
./scripts/upload.hs
git tag <version> && git push --tags
```

Some files were not shown because too many files have changed in this diff Show more