Compare commits
6 commits
Author | SHA1 | Date | |
---|---|---|---|
|
0c48e188f3 | ||
|
4d347edd0e | ||
|
2c30a11e0a | ||
|
63563ca665 | ||
|
84b9ea271e | ||
|
cec89c1cd0 |
446 changed files with 3629 additions and 39857 deletions
12
.github/FUNDING.yml
vendored
12
.github/FUNDING.yml
vendored
|
@ -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
|
14
.github/run-ghcjs-tests.sh
vendored
14
.github/run-ghcjs-tests.sh
vendored
|
@ -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
|
148
.github/workflows/master.yml
vendored
148
.github/workflows/master.yml
vendored
|
@ -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"
|
40
.gitignore
vendored
40
.gitignore
vendored
|
@ -1,40 +0,0 @@
|
|||
**/*/dist
|
||||
*~
|
||||
dist-*
|
||||
.ghc.environment.*
|
||||
/bin
|
||||
/lib
|
||||
/share
|
||||
/packages
|
||||
*-packages.conf.d
|
||||
cabal-dev
|
||||
add-source-timestamps
|
||||
*.o
|
||||
*.hi
|
||||
*.chi
|
||||
*.chs.h
|
||||
*.dyn_o
|
||||
*.dyn_hi
|
||||
.virtualenv
|
||||
.hpc
|
||||
.hsenv
|
||||
.cabal-sandbox/
|
||||
cabal.sandbox.config
|
||||
cabal.config
|
||||
*.prof
|
||||
*.aux
|
||||
*.hp
|
||||
Setup
|
||||
.stack-work
|
||||
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
|
|
@ -1,190 +0,0 @@
|
|||
# stylish-haskell configuration file
|
||||
# ==================================
|
||||
|
||||
# The stylish-haskell tool is mainly configured by specifying steps. These steps
|
||||
# are a list, so they have an order, and one specific step may appear more than
|
||||
# once (if needed). Each file is processed by these steps in the given order.
|
||||
steps:
|
||||
# Convert some ASCII sequences to their Unicode equivalents. This is disabled
|
||||
# by default.
|
||||
# - unicode_syntax:
|
||||
# # In order to make this work, we also need to insert the UnicodeSyntax
|
||||
# # language pragma. If this flag is set to true, we insert it when it's
|
||||
# # not already present. You may want to disable it if you configure
|
||||
# # language extensions using some other method than pragmas. Default:
|
||||
# # 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.
|
||||
#
|
||||
# - global: Align the import names and import list throughout the entire
|
||||
# file.
|
||||
#
|
||||
# - file: Like global, but don't add padding when there are no qualified
|
||||
# imports in the file.
|
||||
#
|
||||
# - group: Only align the imports per group (a group is formed by adjacent
|
||||
# import lines).
|
||||
#
|
||||
# - none: Do not perform any alignment.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# - vertical: Vertical-spaced language pragmas, one per line.
|
||||
#
|
||||
# - compact: A more compact style.
|
||||
#
|
||||
# - compact_line: Similar to compact, but wrap each line with
|
||||
# `{-#LANGUAGE #-}'.
|
||||
#
|
||||
# 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
|
||||
|
||||
# Replace tabs by spaces. This is disabled by default.
|
||||
# - tabs:
|
||||
# # Number of spaces to use for each tab. Default: 8, as specified by the
|
||||
# # Haskell report.
|
||||
# spaces: 8
|
||||
|
||||
# Remove trailing whitespace
|
||||
- trailing_whitespace: {}
|
||||
|
||||
# A common setting is the number of columns (parts of) code will be wrapped
|
||||
# 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
|
117
CONTRIBUTING.md
117
CONTRIBUTING.md
|
@ -1,117 +0,0 @@
|
|||
# Contributing Guidelines
|
||||
|
||||
Contributions are very welcome! To hack on the github version, clone the
|
||||
repository. You can use `cabal`:
|
||||
|
||||
```shell
|
||||
./scripts/start-sandbox.sh # Initialize the sandbox and add-source the packages
|
||||
./scripts/test-all.sh # Run all the tests
|
||||
```
|
||||
|
||||
Or `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
|
||||
```
|
||||
|
||||
Or `nix`:
|
||||
```shell
|
||||
./scripts/generate-nix-files.sh # Get up-to-date shell.nix files
|
||||
```
|
||||
|
||||
To build the docs, see `doc/README.md`.
|
||||
|
||||
## General
|
||||
|
||||
Some things we like:
|
||||
|
||||
- Explicit imports
|
||||
- Upper and lower bounds for packages
|
||||
- Few dependencies
|
||||
- -Werror-compatible (7.8, 7.10 and 8.0)
|
||||
|
||||
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.
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
## PR process
|
||||
|
||||
We try to give timely reviews to PRs that pass CI. If CI for your PR fails, we
|
||||
may close the PR if it has been open for too long (though you should feel free
|
||||
to reopen when the issues have been fixed).
|
||||
|
||||
We require two +1 from the maintainers of the repo. If you feel like there has
|
||||
not been a timely response to a PR, you can ping the Maintainers group (with
|
||||
`@haskell-servant/maintainers`).
|
||||
|
||||
## New combinators
|
||||
|
||||
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).
|
||||
|
||||
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.
|
||||
|
||||
|
||||
## New classes
|
||||
|
||||
The main benefit of having a new class and package in the main servant repo is
|
||||
that we get to see via CI whether changes to other packages break the build.
|
||||
Open an issue to discuss whether a package should be added to the main repo. If
|
||||
we decide that it can, you can still keep maintainership over it.
|
||||
|
||||
Whether or not you want your package to be in the repo, create an issue with
|
||||
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
|
||||
what you contribute from Hackage fairly soon. However, note that prior to major
|
||||
releases it may take some time in between releases.
|
||||
|
||||
## Reporting security issues
|
||||
|
||||
Please email haskell-servant-maintainers AT googlegroups DOT com. This group is
|
||||
private, and accessible only to known maintainers. We will then discuss how to
|
||||
proceed. Please do not make the issue public before we inform you that we have
|
||||
a patch ready.
|
71
README.md
71
README.md
|
@ -1,71 +0,0 @@
|
|||
# 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.
|
||||
|
||||
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/).
|
||||
|
||||
If you need help, drop by the IRC channel (#haskell-servant on libera.chat) 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)
|
13
Servant-API-Alternative.html
Normal file
13
Servant-API-Alternative.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><title>Servant.API.Alternative</title><link href="ocean.css" rel="stylesheet" type="text/css" title="Ocean" /><script src="haddock-util.js" type="text/javascript"></script><script type="text/javascript">//<![CDATA[
|
||||
window.onload = function () {pageLoad();setSynopsis("mini_Servant-API-Alternative.html");};
|
||||
//]]>
|
||||
</script></head><body><div id="package-header"><ul class="links" id="page-menu"><li><a href="src/Servant-API-Alternative.html">Source</a></li><li><a href="index.html">Contents</a></li><li><a href="doc-index.html">Index</a></li></ul><p class="caption">servant-0.2: A family of combinators for defining webservices APIs and serving them</p></div><div id="content"><div id="module-header"><table class="info"><tr><th>Safe Haskell</th><td>None</td></tr><tr><th>Language</th><td>Haskell2010</td></tr></table><p class="caption">Servant.API.Alternative</p></div><div id="synopsis"><p id="control.syn" class="caption expander" onclick="toggleSection('syn')">Synopsis</p><ul id="section.syn" class="hide" onclick="toggleSection('syn')"><li class="src short"><span class="keyword">data</span> a <a href="#t::-60--124--62-">:<|></a> b = a <a href="#v::-60--124--62-">:<|></a> b</li></ul></div><div id="interface"><h1>Documentation</h1><div class="top"><p class="src"><span class="keyword">data</span> a <a name="t::-60--124--62-" class="def">:<|></a> b <span class="fixity">infixr 8</span><span class="rightedge"></span> <a href="src/Servant-API-Alternative.html#%3A%3C%7C%3E" class="link">Source</a></p><div class="doc"><p>Union of two APIs, first takes precedence in case of overlap.</p><p>Example:</p><pre>type MyApi = "books" :> Get [Book] -- GET /books
|
||||
:<|> "books" :> ReqBody Book :> Post Book -- POST /books</pre></div><div class="subs constructors"><p class="caption">Constructors</p><table><tr><td class="src">a <a name="v::-60--124--62-" class="def">:<|></a> b <span class="fixity">infixr 8</span><span class="rightedge"></span></td><td class="doc empty"> </td></tr></table></div><div class="subs instances"><p id="control.i::-60--124--62-" class="caption collapser" onclick="toggleSection('i::-60--124--62-')">Instances</p><div id="section.i::-60--124--62-" class="show"><table><tr><td class="src">(<a href="Servant-Server.html#t:HasServer">HasServer</a> a, <a href="Servant-Server.html#t:HasServer">HasServer</a> b) => <a href="Servant-Server.html#t:HasServer">HasServer</a> (<a href="Servant-API-Alternative.html#t::-60--124--62-">(:<|>)</a> a b)</td><td class="doc"><p>A server for <code>a <code><a href="Servant-API-Alternative.html#t::-60--124--62-">:<|></a></code> b</code> first tries to match the request again the route
|
||||
represented by <code>a</code> and if it fails tries <code>b</code>. You must provide a request
|
||||
handler for each route.</p><pre>type MyApi = "books" :> Get [Book] -- GET /books
|
||||
:<|> "books" :> ReqBody Book :> Post Book -- POST /books
|
||||
|
||||
server :: Server MyApi
|
||||
server = listAllBooks :<|> postBook
|
||||
where listAllBooks = ...
|
||||
postBook book = ...</pre></td></tr><tr><td class="src"><span class="keyword">type</span> <a href="Servant-Server.html#t:Server">Server</a> (<a href="Servant-API-Alternative.html#t::-60--124--62-">(:<|>)</a> a b) = <a href="Servant-API-Alternative.html#t::-60--124--62-">(:<|>)</a> (<a href="Servant-Server.html#t:Server">Server</a> a) (<a href="Servant-Server.html#t:Server">Server</a> b)</td><td class="doc empty"> </td></tr></table></div></div></div></div></div><div id="footer"><p>Produced by <a href="http://www.haskell.org/haddock/">Haddock</a> version 2.15.0</p></div></body></html>
|
15
Servant-API-Capture.html
Normal file
15
Servant-API-Capture.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><title>Servant.API.Capture</title><link href="ocean.css" rel="stylesheet" type="text/css" title="Ocean" /><script src="haddock-util.js" type="text/javascript"></script><script type="text/javascript">//<![CDATA[
|
||||
window.onload = function () {pageLoad();setSynopsis("mini_Servant-API-Capture.html");};
|
||||
//]]>
|
||||
</script></head><body><div id="package-header"><ul class="links" id="page-menu"><li><a href="src/Servant-API-Capture.html">Source</a></li><li><a href="index.html">Contents</a></li><li><a href="doc-index.html">Index</a></li></ul><p class="caption">servant-0.2: A family of combinators for defining webservices APIs and serving them</p></div><div id="content"><div id="module-header"><table class="info"><tr><th>Safe Haskell</th><td>None</td></tr><tr><th>Language</th><td>Haskell2010</td></tr></table><p class="caption">Servant.API.Capture</p></div><div id="synopsis"><p id="control.syn" class="caption expander" onclick="toggleSection('syn')">Synopsis</p><ul id="section.syn" class="hide" onclick="toggleSection('syn')"><li class="src short"><span class="keyword">data</span> <a href="#t:Capture">Capture</a> sym a</li></ul></div><div id="interface"><h1>Documentation</h1><div class="top"><p class="src"><span class="keyword">data</span> <a name="t:Capture" class="def">Capture</a> sym a <a href="src/Servant-API-Capture.html#Capture" class="link">Source</a></p><div class="doc"><p>Capture a value from the request path under a certain type <code>a</code>.</p><p>Example:</p><pre> -- GET /books/:isbn
|
||||
type MyApi = "books" :> Capture "isbn" Text :> Get Book</pre></div><div class="subs instances"><p id="control.i:Capture" class="caption collapser" onclick="toggleSection('i:Capture')">Instances</p><div id="section.i:Capture" class="show"><table><tr><td class="src">(<a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:KnownSymbol">KnownSymbol</a> capture, <a href="Servant-Common-Text.html#t:FromText">FromText</a> a, <a href="Servant-Server.html#t:HasServer">HasServer</a> sublayout) => <a href="Servant-Server.html#t:HasServer">HasServer</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-Capture.html#t:Capture">Capture</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:Symbol">Symbol</a> * capture a) sublayout)</td><td class="doc"><p>If you use <code><a href="Servant-API-Capture.html#t:Capture">Capture</a></code> in one of the endpoints for your API,
|
||||
this automatically requires your server-side handler to be a function
|
||||
that takes an argument of the type specified by the <code><a href="Servant-API-Capture.html#t:Capture">Capture</a></code>.
|
||||
This lets servant worry about getting it from the URL and turning
|
||||
it into a value of the type you specify.</p><p>You can control how it'll be converted from <code><a href="https://hackage.haskell.org/package/text-1.2.0.0/docs/Data-Text.html#t:Text">Text</a></code> to your type
|
||||
by simply providing an instance of <code><a href="Servant-Common-Text.html#t:FromText">FromText</a></code> for your type.</p><p>Example:</p><pre>type MyApi = "books" :> Capture "isbn" Text :> Get Book
|
||||
|
||||
server :: Server MyApi
|
||||
server = getBook
|
||||
where getBook :: Text -> EitherT (Int, String) IO Book
|
||||
getBook isbn = ...</pre></td></tr><tr><td class="src"><span class="keyword">type</span> <a href="Servant-Server.html#t:Server">Server</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-Capture.html#t:Capture">Capture</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:Symbol">Symbol</a> * capture a) sublayout) = a -> <a href="Servant-Server.html#t:Server">Server</a> sublayout</td><td class="doc empty"> </td></tr></table></div></div></div></div></div><div id="footer"><p>Produced by <a href="http://www.haskell.org/haddock/">Haddock</a> version 2.15.0</p></div></body></html>
|
13
Servant-API-Delete.html
Normal file
13
Servant-API-Delete.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><title>Servant.API.Delete</title><link href="ocean.css" rel="stylesheet" type="text/css" title="Ocean" /><script src="haddock-util.js" type="text/javascript"></script><script type="text/javascript">//<![CDATA[
|
||||
window.onload = function () {pageLoad();setSynopsis("mini_Servant-API-Delete.html");};
|
||||
//]]>
|
||||
</script></head><body><div id="package-header"><ul class="links" id="page-menu"><li><a href="src/Servant-API-Delete.html">Source</a></li><li><a href="index.html">Contents</a></li><li><a href="doc-index.html">Index</a></li></ul><p class="caption">servant-0.2: A family of combinators for defining webservices APIs and serving them</p></div><div id="content"><div id="module-header"><table class="info"><tr><th>Safe Haskell</th><td>None</td></tr><tr><th>Language</th><td>Haskell2010</td></tr></table><p class="caption">Servant.API.Delete</p></div><div id="synopsis"><p id="control.syn" class="caption expander" onclick="toggleSection('syn')">Synopsis</p><ul id="section.syn" class="hide" onclick="toggleSection('syn')"><li class="src short"><span class="keyword">data</span> <a href="#t:Delete">Delete</a></li></ul></div><div id="interface"><h1>Documentation</h1><div class="top"><p class="src"><span class="keyword">data</span> <a name="t:Delete" class="def">Delete</a> <a href="src/Servant-API-Delete.html#Delete" class="link">Source</a></p><div class="doc"><p>Combinator for DELETE requests.</p><p>Example:</p><pre> -- DELETE /books/:isbn
|
||||
type MyApi = "books" :> Capture "isbn" Text :> Delete</pre></div><div class="subs instances"><p id="control.i:Delete" class="caption collapser" onclick="toggleSection('i:Delete')">Instances</p><div id="section.i:Delete" class="show"><table><tr><td class="src"><a href="Servant-Server.html#t:HasServer">HasServer</a> <a href="Servant-API-Delete.html#t:Delete">Delete</a></td><td class="doc"><p>If you have a <code><a href="Servant-API-Delete.html#t:Delete">Delete</a></code> endpoint in your API,
|
||||
the handler for this endpoint is meant to delete
|
||||
a resource.</p><p>The code of the handler will, just like
|
||||
for <code><a href="Servant-API-Get.html#t:Get">Get</a></code>, <code><a href="Servant-API-Post.html#t:Post">Post</a></code> and
|
||||
<code><a href="Servant-API-Put.html#t:Put">Put</a></code>, run in <code>EitherT (Int, String) IO ()</code>.
|
||||
The <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Int.html#t:Int">Int</a></code> represents the status code and the <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-String.html#t:String">String</a></code> a message
|
||||
to be returned. You can use <code><a href="https://hackage.haskell.org/package/either-4.3.2/docs/Control-Monad-Trans-Either.html#v:left">left</a></code> to
|
||||
painlessly error out if the conditions for a successful deletion
|
||||
are not met.</p></td></tr><tr><td class="src"><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Typeable-Internal.html#t:Typeable">Typeable</a> * <a href="Servant-API-Delete.html#t:Delete">Delete</a></td><td class="doc empty"> </td></tr><tr><td class="src"><span class="keyword">type</span> <a href="Servant-Server.html#t:Server">Server</a> <a href="Servant-API-Delete.html#t:Delete">Delete</a> = <a href="https://hackage.haskell.org/package/either-4.3.2/docs/Control-Monad-Trans-Either.html#t:EitherT">EitherT</a> (<a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Int.html#t:Int">Int</a>, <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-String.html#t:String">String</a>) <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/System-IO.html#t:IO">IO</a> ()</td><td class="doc empty"> </td></tr></table></div></div></div></div></div><div id="footer"><p>Produced by <a href="http://www.haskell.org/haddock/">Haddock</a> version 2.15.0</p></div></body></html>
|
12
Servant-API-Get.html
Normal file
12
Servant-API-Get.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><title>Servant.API.Get</title><link href="ocean.css" rel="stylesheet" type="text/css" title="Ocean" /><script src="haddock-util.js" type="text/javascript"></script><script type="text/javascript">//<![CDATA[
|
||||
window.onload = function () {pageLoad();setSynopsis("mini_Servant-API-Get.html");};
|
||||
//]]>
|
||||
</script></head><body><div id="package-header"><ul class="links" id="page-menu"><li><a href="src/Servant-API-Get.html">Source</a></li><li><a href="index.html">Contents</a></li><li><a href="doc-index.html">Index</a></li></ul><p class="caption">servant-0.2: A family of combinators for defining webservices APIs and serving them</p></div><div id="content"><div id="module-header"><table class="info"><tr><th>Safe Haskell</th><td>None</td></tr><tr><th>Language</th><td>Haskell2010</td></tr></table><p class="caption">Servant.API.Get</p></div><div id="synopsis"><p id="control.syn" class="caption expander" onclick="toggleSection('syn')">Synopsis</p><ul id="section.syn" class="hide" onclick="toggleSection('syn')"><li class="src short"><span class="keyword">data</span> <a href="#t:Get">Get</a> a</li></ul></div><div id="interface"><h1>Documentation</h1><div class="top"><p class="src"><span class="keyword">data</span> <a name="t:Get" class="def">Get</a> a <a href="src/Servant-API-Get.html#Get" class="link">Source</a></p><div class="doc"><p>Endpoint for simple GET requests. Serves the result as JSON.</p><p>Example:</p><pre>type MyApi = "books" :> Get [Book]</pre></div><div class="subs instances"><p id="control.i:Get" class="caption collapser" onclick="toggleSection('i:Get')">Instances</p><div id="section.i:Get" class="show"><table><tr><td class="src"><a href="Servant-Utils-Links.html#t:VLinkHelper">VLinkHelper</a> * (<a href="Servant-API-Get.html#t:Get">Get</a> x)</td><td class="doc empty"> </td></tr><tr><td class="src"><a href="https://hackage.haskell.org/package/aeson-0.8.0.2/docs/Data-Aeson-Types.html#t:ToJSON">ToJSON</a> result => <a href="Servant-Server.html#t:HasServer">HasServer</a> (<a href="Servant-API-Get.html#t:Get">Get</a> result)</td><td class="doc"><p>When implementing the handler for a <code><a href="Servant-API-Get.html#t:Get">Get</a></code> endpoint,
|
||||
just like for <code><a href="Servant-API-Delete.html#t:Delete">Delete</a></code>, <code><a href="Servant-API-Post.html#t:Post">Post</a></code>
|
||||
and <code><a href="Servant-API-Put.html#t:Put">Put</a></code>, the handler code runs in the
|
||||
<code>EitherT (Int, String) IO</code> monad, where the <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Int.html#t:Int">Int</a></code> represents
|
||||
the status code and the <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-String.html#t:String">String</a></code> a message, returned in case of
|
||||
failure. You can quite handily use <code><a href="Control-Monad-Trans-EitherT.html#v:left">left</a></code>
|
||||
to quickly fail if some conditions are not met.</p><p>If successfully returning a value, we just require that its type has
|
||||
a <code><a href="https://hackage.haskell.org/package/aeson-0.8.0.2/docs/Data-Aeson-Types.html#t:ToJSON">ToJSON</a></code> instance and servant takes care of encoding it for you,
|
||||
yielding status code 200 along the way.</p></td></tr><tr><td class="src"><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Typeable-Internal.html#t:Typeable">Typeable</a> (* -> *) <a href="Servant-API-Get.html#t:Get">Get</a></td><td class="doc empty"> </td></tr><tr><td class="src"><span class="keyword">type</span> <a href="Servant-Server.html#t:Server">Server</a> (<a href="Servant-API-Get.html#t:Get">Get</a> result) = <a href="https://hackage.haskell.org/package/either-4.3.2/docs/Control-Monad-Trans-Either.html#t:EitherT">EitherT</a> (<a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Int.html#t:Int">Int</a>, <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-String.html#t:String">String</a>) <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/System-IO.html#t:IO">IO</a> result</td><td class="doc empty"> </td></tr></table></div></div></div></div></div><div id="footer"><p>Produced by <a href="http://www.haskell.org/haddock/">Haddock</a> version 2.15.0</p></div></body></html>
|
21
Servant-API-Header.html
Normal file
21
Servant-API-Header.html
Normal file
|
@ -0,0 +1,21 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><title>Servant.API.Header</title><link href="ocean.css" rel="stylesheet" type="text/css" title="Ocean" /><script src="haddock-util.js" type="text/javascript"></script><script type="text/javascript">//<![CDATA[
|
||||
window.onload = function () {pageLoad();setSynopsis("mini_Servant-API-Header.html");};
|
||||
//]]>
|
||||
</script></head><body><div id="package-header"><ul class="links" id="page-menu"><li><a href="src/Servant-API-Header.html">Source</a></li><li><a href="index.html">Contents</a></li><li><a href="doc-index.html">Index</a></li></ul><p class="caption">servant-0.2: A family of combinators for defining webservices APIs and serving them</p></div><div id="content"><div id="module-header"><table class="info"><tr><th>Safe Haskell</th><td>None</td></tr><tr><th>Language</th><td>Haskell2010</td></tr></table><p class="caption">Servant.API.Header</p></div><div id="synopsis"><p id="control.syn" class="caption expander" onclick="toggleSection('syn')">Synopsis</p><ul id="section.syn" class="hide" onclick="toggleSection('syn')"><li class="src short"><span class="keyword">data</span> <a href="#t:Header">Header</a> sym a</li></ul></div><div id="interface"><h1>Documentation</h1><div class="top"><p class="src"><span class="keyword">data</span> <a name="t:Header" class="def">Header</a> sym a <a href="src/Servant-API-Header.html#Header" class="link">Source</a></p><div class="doc"><p>Extract the given header's value as a value of type <code>a</code>.</p><p>Example:</p><pre>newtype Referer = Referer Text
|
||||
deriving (Eq, Show, FromText, ToText)
|
||||
|
||||
-- GET /view-my-referer
|
||||
type MyApi = "view-my-referer" :> Header "from" Referer :> Get Referer</pre></div><div class="subs instances"><p id="control.i:Header" class="caption collapser" onclick="toggleSection('i:Header')">Instances</p><div id="section.i:Header" class="show"><table><tr><td class="src">(<a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:KnownSymbol">KnownSymbol</a> sym, <a href="Servant-Common-Text.html#t:FromText">FromText</a> a, <a href="Servant-Server.html#t:HasServer">HasServer</a> sublayout) => <a href="Servant-Server.html#t:HasServer">HasServer</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-Header.html#t:Header">Header</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:Symbol">Symbol</a> * sym a) sublayout)</td><td class="doc"><p>If you use <code><a href="Servant-API-Header.html#t:Header">Header</a></code> in one of the endpoints for your API,
|
||||
this automatically requires your server-side handler to be a function
|
||||
that takes an argument of the type specified by <code><a href="Servant-API-Header.html#t:Header">Header</a></code>.
|
||||
This lets servant worry about extracting it from the request and turning
|
||||
it into a value of the type you specify.</p><p>All it asks is for a <code><a href="Servant-Common-Text.html#t:FromText">FromText</a></code> instance.</p><p>Example:</p><pre>newtype Referer = Referer Text
|
||||
deriving (Eq, Show, FromText, ToText)
|
||||
|
||||
-- GET /view-my-referer
|
||||
type MyApi = "view-my-referer" :> Header "Referer" Referer :> Get Referer
|
||||
|
||||
server :: Server MyApi
|
||||
server = viewReferer
|
||||
where viewReferer :: Referer -> EitherT (Int, String) IO referer
|
||||
viewReferer referer = return referer</pre></td></tr><tr><td class="src"><span class="keyword">type</span> <a href="Servant-Server.html#t:Server">Server</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-Header.html#t:Header">Header</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:Symbol">Symbol</a> * sym a) sublayout) = <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Maybe.html#t:Maybe">Maybe</a> a -> <a href="Servant-Server.html#t:Server">Server</a> sublayout</td><td class="doc empty"> </td></tr></table></div></div></div></div></div><div id="footer"><p>Produced by <a href="http://www.haskell.org/haddock/">Haddock</a> version 2.15.0</p></div></body></html>
|
17
Servant-API-Post.html
Normal file
17
Servant-API-Post.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><title>Servant.API.Post</title><link href="ocean.css" rel="stylesheet" type="text/css" title="Ocean" /><script src="haddock-util.js" type="text/javascript"></script><script type="text/javascript">//<![CDATA[
|
||||
window.onload = function () {pageLoad();setSynopsis("mini_Servant-API-Post.html");};
|
||||
//]]>
|
||||
</script></head><body><div id="package-header"><ul class="links" id="page-menu"><li><a href="src/Servant-API-Post.html">Source</a></li><li><a href="index.html">Contents</a></li><li><a href="doc-index.html">Index</a></li></ul><p class="caption">servant-0.2: A family of combinators for defining webservices APIs and serving them</p></div><div id="content"><div id="module-header"><table class="info"><tr><th>Safe Haskell</th><td>None</td></tr><tr><th>Language</th><td>Haskell2010</td></tr></table><p class="caption">Servant.API.Post</p></div><div id="synopsis"><p id="control.syn" class="caption expander" onclick="toggleSection('syn')">Synopsis</p><ul id="section.syn" class="hide" onclick="toggleSection('syn')"><li class="src short"><span class="keyword">data</span> <a href="#t:Post">Post</a> a</li></ul></div><div id="interface"><h1>Documentation</h1><div class="top"><p class="src"><span class="keyword">data</span> <a name="t:Post" class="def">Post</a> a <a href="src/Servant-API-Post.html#Post" class="link">Source</a></p><div class="doc"><p>Endpoint for POST requests. The type variable represents the type of the
|
||||
response body (not the request body, use <code><a href="Servant-API-RQBody.html#t:RQBody">RQBody</a></code> for
|
||||
that).</p><p>Example:</p><pre> -- POST /books
|
||||
-- with a JSON encoded Book as the request body
|
||||
-- returning the just-created Book
|
||||
type MyApi = "books" :> ReqBody Book :> Post Book</pre></div><div class="subs instances"><p id="control.i:Post" class="caption collapser" onclick="toggleSection('i:Post')">Instances</p><div id="section.i:Post" class="show"><table><tr><td class="src"><a href="Servant-Utils-Links.html#t:VLinkHelper">VLinkHelper</a> * (<a href="Servant-API-Post.html#t:Post">Post</a> x)</td><td class="doc empty"> </td></tr><tr><td class="src"><a href="https://hackage.haskell.org/package/aeson-0.8.0.2/docs/Data-Aeson-Types.html#t:ToJSON">ToJSON</a> a => <a href="Servant-Server.html#t:HasServer">HasServer</a> (<a href="Servant-API-Post.html#t:Post">Post</a> a)</td><td class="doc"><p>When implementing the handler for a <code><a href="Servant-API-Post.html#t:Post">Post</a></code> endpoint,
|
||||
just like for <code><a href="Servant-API-Delete.html#t:Delete">Delete</a></code>, <code><a href="Servant-API-Get.html#t:Get">Get</a></code>
|
||||
and <code><a href="Servant-API-Put.html#t:Put">Put</a></code>, the handler code runs in the
|
||||
<code>EitherT (Int, String) IO</code> monad, where the <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Int.html#t:Int">Int</a></code> represents
|
||||
the status code and the <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-String.html#t:String">String</a></code> a message, returned in case of
|
||||
failure. You can quite handily use <code><a href="Control-Monad-Trans-EitherT.html#v:left">left</a></code>
|
||||
to quickly fail if some conditions are not met.</p><p>If successfully returning a value, we just require that its type has
|
||||
a <code><a href="https://hackage.haskell.org/package/aeson-0.8.0.2/docs/Data-Aeson-Types.html#t:ToJSON">ToJSON</a></code> instance and servant takes care of encoding it for you,
|
||||
yielding status code 201 along the way.</p></td></tr><tr><td class="src"><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Typeable-Internal.html#t:Typeable">Typeable</a> (* -> *) <a href="Servant-API-Post.html#t:Post">Post</a></td><td class="doc empty"> </td></tr><tr><td class="src"><span class="keyword">type</span> <a href="Servant-Server.html#t:Server">Server</a> (<a href="Servant-API-Post.html#t:Post">Post</a> a) = <a href="https://hackage.haskell.org/package/either-4.3.2/docs/Control-Monad-Trans-Either.html#t:EitherT">EitherT</a> (<a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Int.html#t:Int">Int</a>, <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-String.html#t:String">String</a>) <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/System-IO.html#t:IO">IO</a> a</td><td class="doc empty"> </td></tr></table></div></div></div></div></div><div id="footer"><p>Produced by <a href="http://www.haskell.org/haddock/">Haddock</a> version 2.15.0</p></div></body></html>
|
15
Servant-API-Put.html
Normal file
15
Servant-API-Put.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><title>Servant.API.Put</title><link href="ocean.css" rel="stylesheet" type="text/css" title="Ocean" /><script src="haddock-util.js" type="text/javascript"></script><script type="text/javascript">//<![CDATA[
|
||||
window.onload = function () {pageLoad();setSynopsis("mini_Servant-API-Put.html");};
|
||||
//]]>
|
||||
</script></head><body><div id="package-header"><ul class="links" id="page-menu"><li><a href="src/Servant-API-Put.html">Source</a></li><li><a href="index.html">Contents</a></li><li><a href="doc-index.html">Index</a></li></ul><p class="caption">servant-0.2: A family of combinators for defining webservices APIs and serving them</p></div><div id="content"><div id="module-header"><table class="info"><tr><th>Safe Haskell</th><td>None</td></tr><tr><th>Language</th><td>Haskell2010</td></tr></table><p class="caption">Servant.API.Put</p></div><div id="synopsis"><p id="control.syn" class="caption expander" onclick="toggleSection('syn')">Synopsis</p><ul id="section.syn" class="hide" onclick="toggleSection('syn')"><li class="src short"><span class="keyword">data</span> <a href="#t:Put">Put</a> a</li></ul></div><div id="interface"><h1>Documentation</h1><div class="top"><p class="src"><span class="keyword">data</span> <a name="t:Put" class="def">Put</a> a <a href="src/Servant-API-Put.html#Put" class="link">Source</a></p><div class="doc"><p>Endpoint for PUT requests, usually used to update a ressource.
|
||||
The type <code>a</code> is the type of the response body that's returned.</p><p>Example:</p><pre>-- PUT /books/:isbn
|
||||
-- with a Book as request body, returning the updated Book
|
||||
type MyApi = "books" :> Capture "isbn" Text :> ReqBody Book :> Put Book</pre></div><div class="subs instances"><p id="control.i:Put" class="caption collapser" onclick="toggleSection('i:Put')">Instances</p><div id="section.i:Put" class="show"><table><tr><td class="src"><a href="https://hackage.haskell.org/package/aeson-0.8.0.2/docs/Data-Aeson-Types.html#t:ToJSON">ToJSON</a> a => <a href="Servant-Server.html#t:HasServer">HasServer</a> (<a href="Servant-API-Put.html#t:Put">Put</a> a)</td><td class="doc"><p>When implementing the handler for a <code><a href="Servant-API-Put.html#t:Put">Put</a></code> endpoint,
|
||||
just like for <code><a href="Servant-API-Delete.html#t:Delete">Delete</a></code>, <code><a href="Servant-API-Get.html#t:Get">Get</a></code>
|
||||
and <code><a href="Servant-API-Post.html#t:Post">Post</a></code>, the handler code runs in the
|
||||
<code>EitherT (Int, String) IO</code> monad, where the <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Int.html#t:Int">Int</a></code> represents
|
||||
the status code and the <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-String.html#t:String">String</a></code> a message, returned in case of
|
||||
failure. You can quite handily use <code><a href="Control-Monad-Trans-EitherT.html#v:left">left</a></code>
|
||||
to quickly fail if some conditions are not met.</p><p>If successfully returning a value, we just require that its type has
|
||||
a <code><a href="https://hackage.haskell.org/package/aeson-0.8.0.2/docs/Data-Aeson-Types.html#t:ToJSON">ToJSON</a></code> instance and servant takes care of encoding it for you,
|
||||
yielding status code 200 along the way.</p></td></tr><tr><td class="src"><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Typeable-Internal.html#t:Typeable">Typeable</a> (* -> *) <a href="Servant-API-Put.html#t:Put">Put</a></td><td class="doc empty"> </td></tr><tr><td class="src"><span class="keyword">type</span> <a href="Servant-Server.html#t:Server">Server</a> (<a href="Servant-API-Put.html#t:Put">Put</a> a) = <a href="https://hackage.haskell.org/package/either-4.3.2/docs/Control-Monad-Trans-Either.html#t:EitherT">EitherT</a> (<a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Int.html#t:Int">Int</a>, <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-String.html#t:String">String</a>) <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/System-IO.html#t:IO">IO</a> a</td><td class="doc empty"> </td></tr></table></div></div></div></div></div><div id="footer"><p>Produced by <a href="http://www.haskell.org/haddock/">Haddock</a> version 2.15.0</p></div></body></html>
|
45
Servant-API-QueryParam.html
Normal file
45
Servant-API-QueryParam.html
Normal file
|
@ -0,0 +1,45 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><title>Servant.API.QueryParam</title><link href="ocean.css" rel="stylesheet" type="text/css" title="Ocean" /><script src="haddock-util.js" type="text/javascript"></script><script type="text/javascript">//<![CDATA[
|
||||
window.onload = function () {pageLoad();setSynopsis("mini_Servant-API-QueryParam.html");};
|
||||
//]]>
|
||||
</script></head><body><div id="package-header"><ul class="links" id="page-menu"><li><a href="src/Servant-API-QueryParam.html">Source</a></li><li><a href="index.html">Contents</a></li><li><a href="doc-index.html">Index</a></li></ul><p class="caption">servant-0.2: A family of combinators for defining webservices APIs and serving them</p></div><div id="content"><div id="module-header"><table class="info"><tr><th>Safe Haskell</th><td>None</td></tr><tr><th>Language</th><td>Haskell2010</td></tr></table><p class="caption">Servant.API.QueryParam</p></div><div id="synopsis"><p id="control.syn" class="caption expander" onclick="toggleSection('syn')">Synopsis</p><ul id="section.syn" class="hide" onclick="toggleSection('syn')"><li class="src short"><span class="keyword">data</span> <a href="#t:QueryParam">QueryParam</a> sym a</li><li class="src short"><span class="keyword">data</span> <a href="#t:QueryParams">QueryParams</a> sym a</li><li class="src short"><span class="keyword">data</span> <a href="#t:QueryFlag">QueryFlag</a> sym</li></ul></div><div id="interface"><h1>Documentation</h1><div class="top"><p class="src"><span class="keyword">data</span> <a name="t:QueryParam" class="def">QueryParam</a> sym a <a href="src/Servant-API-QueryParam.html#QueryParam" class="link">Source</a></p><div class="doc"><p>Lookup the value associated to the <code>sym</code> query string parameter
|
||||
and try to extract it as a value of type <code>a</code>.</p><p>Example:</p><pre>-- /books?author=<author name>
|
||||
type MyApi = "books" :> QueryParam "author" Text :> Get [Book]</pre></div><div class="subs instances"><p id="control.i:QueryParam" class="caption collapser" onclick="toggleSection('i:QueryParam')">Instances</p><div id="section.i:QueryParam" class="show"><table><tr><td class="src">(<a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:KnownSymbol">KnownSymbol</a> sym, <a href="Servant-Common-Text.html#t:FromText">FromText</a> a, <a href="Servant-Server.html#t:HasServer">HasServer</a> sublayout) => <a href="Servant-Server.html#t:HasServer">HasServer</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-QueryParam.html#t:QueryParam">QueryParam</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:Symbol">Symbol</a> * sym a) sublayout)</td><td class="doc"><p>If you use <code><code><a href="Servant-API-QueryParam.html#t:QueryParam">QueryParam</a></code> "author" Text</code> in one of the endpoints for your API,
|
||||
this automatically requires your server-side handler to be a function
|
||||
that takes an argument of type <code><code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Maybe.html#t:Maybe">Maybe</a></code> <code>Text</code></code>.</p><p>This lets servant worry about looking it up in the query string
|
||||
and turning it into a value of the type you specify, enclosed
|
||||
in <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Maybe.html#t:Maybe">Maybe</a></code>, because it may not be there and servant would then
|
||||
hand you <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Maybe.html#v:Nothing">Nothing</a></code>.</p><p>You can control how it'll be converted from <code>Text</code> to your type
|
||||
by simply providing an instance of <code><a href="Servant-Common-Text.html#t:FromText">FromText</a></code> for your type.</p><p>Example:</p><pre>type MyApi = "books" :> QueryParam "author" Text :> Get [Book]
|
||||
|
||||
server :: Server MyApi
|
||||
server = getBooksBy
|
||||
where getBooksBy :: Maybe Text -> EitherT (Int, String) IO [Book]
|
||||
getBooksBy Nothing = ...return all books...
|
||||
getBooksBy (Just author) = ...return books by the given author...</pre></td></tr><tr><td class="src"><span class="keyword">type</span> <a href="Servant-Server.html#t:Server">Server</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-QueryParam.html#t:QueryParam">QueryParam</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:Symbol">Symbol</a> * sym a) sublayout) = <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Maybe.html#t:Maybe">Maybe</a> a -> <a href="Servant-Server.html#t:Server">Server</a> sublayout</td><td class="doc empty"> </td></tr></table></div></div></div><div class="top"><p class="src"><span class="keyword">data</span> <a name="t:QueryParams" class="def">QueryParams</a> sym a <a href="src/Servant-API-QueryParam.html#QueryParams" class="link">Source</a></p><div class="doc"><p>Lookup the values associated to the <code>sym</code> query string parameter
|
||||
and try to extract it as a value of type <code>[a]</code>. This is typically
|
||||
meant to support query string parameters of the form
|
||||
<code>param[]=val1&param[]=val2</code> and so on. Note that servant doesn't actually
|
||||
require the <code>[]</code>s and will fetch the values just fine with
|
||||
<code>param=val1&param=val2</code>, too.</p><p>Example:</p><pre>-- /books?authors[]=<author1>&authors[]=<author2>&...
|
||||
type MyApi = "books" :> QueryParams "authors" Text :> Get [Book]</pre></div><div class="subs instances"><p id="control.i:QueryParams" class="caption collapser" onclick="toggleSection('i:QueryParams')">Instances</p><div id="section.i:QueryParams" class="show"><table><tr><td class="src">(<a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:KnownSymbol">KnownSymbol</a> sym, <a href="Servant-Common-Text.html#t:FromText">FromText</a> a, <a href="Servant-Server.html#t:HasServer">HasServer</a> sublayout) => <a href="Servant-Server.html#t:HasServer">HasServer</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-QueryParam.html#t:QueryParams">QueryParams</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:Symbol">Symbol</a> * sym a) sublayout)</td><td class="doc"><p>If you use <code><code><a href="Servant-API-QueryParam.html#t:QueryParams">QueryParams</a></code> "authors" Text</code> in one of the endpoints for your API,
|
||||
this automatically requires your server-side handler to be a function
|
||||
that takes an argument of type <code>[<code>Text</code>]</code>.</p><p>This lets servant worry about looking up 0 or more values in the query string
|
||||
associated to <code>authors</code> and turning each of them into a value of
|
||||
the type you specify.</p><p>You can control how the individual values are converted from <code>Text</code> to your type
|
||||
by simply providing an instance of <code><a href="Servant-Common-Text.html#t:FromText">FromText</a></code> for your type.</p><p>Example:</p><pre>type MyApi = "books" :> QueryParams "authors" Text :> Get [Book]
|
||||
|
||||
server :: Server MyApi
|
||||
server = getBooksBy
|
||||
where getBooksBy :: [Text] -> EitherT (Int, String) IO [Book]
|
||||
getBooksBy authors = ...return all books by these authors...</pre></td></tr><tr><td class="src"><span class="keyword">type</span> <a href="Servant-Server.html#t:Server">Server</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-QueryParam.html#t:QueryParams">QueryParams</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:Symbol">Symbol</a> * sym a) sublayout) = [a] -> <a href="Servant-Server.html#t:Server">Server</a> sublayout</td><td class="doc empty"> </td></tr></table></div></div></div><div class="top"><p class="src"><span class="keyword">data</span> <a name="t:QueryFlag" class="def">QueryFlag</a> sym <a href="src/Servant-API-QueryParam.html#QueryFlag" class="link">Source</a></p><div class="doc"><p>Lookup a potentially value-less query string parameter
|
||||
with boolean semantics. If the param <code>sym</code> is there without any value,
|
||||
or if it's there with value "true" or "1", it's interpreted as <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Bool.html#v:True">True</a></code>.
|
||||
Otherwise, it's interpreted as <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Bool.html#v:False">False</a></code>.</p><p>Example:</p><pre>-- /books?published
|
||||
type MyApi = "books" :> QueryFlag "published" :> Get [Book]</pre></div><div class="subs instances"><p id="control.i:QueryFlag" class="caption collapser" onclick="toggleSection('i:QueryFlag')">Instances</p><div id="section.i:QueryFlag" class="show"><table><tr><td class="src">(<a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:KnownSymbol">KnownSymbol</a> sym, <a href="Servant-Server.html#t:HasServer">HasServer</a> sublayout) => <a href="Servant-Server.html#t:HasServer">HasServer</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-QueryParam.html#t:QueryFlag">QueryFlag</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:Symbol">Symbol</a> sym) sublayout)</td><td class="doc"><p>If you use <code><code><a href="Servant-API-QueryParam.html#t:QueryFlag">QueryFlag</a></code> "published"</code> in one of the endpoints for your API,
|
||||
this automatically requires your server-side handler to be a function
|
||||
that takes an argument of type <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Bool.html#t:Bool">Bool</a></code>.</p><p>Example:</p><pre>type MyApi = "books" :> QueryFlag "published" :> Get [Book]
|
||||
|
||||
server :: Server MyApi
|
||||
server = getBooks
|
||||
where getBooks :: Bool -> EitherT (Int, String) IO [Book]
|
||||
getBooks onlyPublished = ...return all books, or only the ones that are already published, depending on the argument...</pre></td></tr><tr><td class="src"><span class="keyword">type</span> <a href="Servant-Server.html#t:Server">Server</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-QueryParam.html#t:QueryFlag">QueryFlag</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:Symbol">Symbol</a> sym) sublayout) = <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Bool.html#t:Bool">Bool</a> -> <a href="Servant-Server.html#t:Server">Server</a> sublayout</td><td class="doc empty"> </td></tr></table></div></div></div></div></div><div id="footer"><p>Produced by <a href="http://www.haskell.org/haddock/">Haddock</a> version 2.15.0</p></div></body></html>
|
11
Servant-API-Raw.html
Normal file
11
Servant-API-Raw.html
Normal file
|
@ -0,0 +1,11 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><title>Servant.API.Raw</title><link href="ocean.css" rel="stylesheet" type="text/css" title="Ocean" /><script src="haddock-util.js" type="text/javascript"></script><script type="text/javascript">//<![CDATA[
|
||||
window.onload = function () {pageLoad();setSynopsis("mini_Servant-API-Raw.html");};
|
||||
//]]>
|
||||
</script></head><body><div id="package-header"><ul class="links" id="page-menu"><li><a href="src/Servant-API-Raw.html">Source</a></li><li><a href="index.html">Contents</a></li><li><a href="doc-index.html">Index</a></li></ul><p class="caption">servant-0.2: A family of combinators for defining webservices APIs and serving them</p></div><div id="content"><div id="module-header"><table class="info"><tr><th>Safe Haskell</th><td>None</td></tr><tr><th>Language</th><td>Haskell2010</td></tr></table><p class="caption">Servant.API.Raw</p></div><div id="synopsis"><p id="control.syn" class="caption expander" onclick="toggleSection('syn')">Synopsis</p><ul id="section.syn" class="hide" onclick="toggleSection('syn')"><li class="src short"><span class="keyword">data</span> <a href="#t:Raw">Raw</a></li></ul></div><div id="interface"><h1>Documentation</h1><div class="top"><p class="src"><span class="keyword">data</span> <a name="t:Raw" class="def">Raw</a> <a href="src/Servant-API-Raw.html#Raw" class="link">Source</a></p><div class="doc"><p>Endpoint for plugging in your own Wai <code><a href="https://hackage.haskell.org/package/wai-3.0.2/docs/Network-Wai.html#t:Application">Application</a></code>s.</p><p>The given <code><a href="https://hackage.haskell.org/package/wai-3.0.2/docs/Network-Wai.html#t:Application">Application</a></code> will get the request as received by the server, potentially with
|
||||
a modified (stripped) <code><a href="https://hackage.haskell.org/package/wai-3.0.2/docs/Network-Wai.html#v:pathInfo">pathInfo</a></code> if the <code><a href="https://hackage.haskell.org/package/wai-3.0.2/docs/Network-Wai.html#t:Application">Application</a></code> is being routed with <code><a href="Servant-API-Sub.html#t::-62-">:></a></code>.</p><p>In addition to just letting you plug in your existing WAI <code><a href="https://hackage.haskell.org/package/wai-3.0.2/docs/Network-Wai.html#t:Application">Application</a></code>s,
|
||||
this can also be used with <code><a href="Servant-Utils-StaticFiles.html#v:serveDirectory">serveDirectory</a></code> to serve
|
||||
static files stored in a particular directory on your filesystem, or to serve
|
||||
your API's documentation with <code><a href="Servant-Utils-StaticFiles.html#v:serveDocumentation">serveDocumentation</a></code>.</p></div><div class="subs instances"><p id="control.i:Raw" class="caption collapser" onclick="toggleSection('i:Raw')">Instances</p><div id="section.i:Raw" class="show"><table><tr><td class="src"><a href="Servant-Server.html#t:HasServer">HasServer</a> <a href="Servant-API-Raw.html#t:Raw">Raw</a></td><td class="doc"><p>Just pass the request to the underlying application and serve its response.</p><p>Example:</p><pre>type MyApi = "images" :> Raw
|
||||
|
||||
server :: Server MyApi
|
||||
server = serveDirectory "/var/www/images"</pre></td></tr><tr><td class="src"><span class="keyword">type</span> <a href="Servant-Server.html#t:Server">Server</a> <a href="Servant-API-Raw.html#t:Raw">Raw</a> = <a href="https://hackage.haskell.org/package/wai-3.0.2/docs/Network-Wai.html#t:Application">Application</a></td><td class="doc empty"> </td></tr></table></div></div></div></div></div><div id="footer"><p>Produced by <a href="http://www.haskell.org/haddock/">Haddock</a> version 2.15.0</p></div></body></html>
|
14
Servant-API-ReqBody.html
Normal file
14
Servant-API-ReqBody.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><title>Servant.API.ReqBody</title><link href="ocean.css" rel="stylesheet" type="text/css" title="Ocean" /><script src="haddock-util.js" type="text/javascript"></script><script type="text/javascript">//<![CDATA[
|
||||
window.onload = function () {pageLoad();setSynopsis("mini_Servant-API-ReqBody.html");};
|
||||
//]]>
|
||||
</script></head><body><div id="package-header"><ul class="links" id="page-menu"><li><a href="src/Servant-API-ReqBody.html">Source</a></li><li><a href="index.html">Contents</a></li><li><a href="doc-index.html">Index</a></li></ul><p class="caption">servant-0.2: A family of combinators for defining webservices APIs and serving them</p></div><div id="content"><div id="module-header"><table class="info"><tr><th>Safe Haskell</th><td>None</td></tr><tr><th>Language</th><td>Haskell2010</td></tr></table><p class="caption">Servant.API.ReqBody</p></div><div id="synopsis"><p id="control.syn" class="caption expander" onclick="toggleSection('syn')">Synopsis</p><ul id="section.syn" class="hide" onclick="toggleSection('syn')"><li class="src short"><span class="keyword">data</span> <a href="#t:ReqBody">ReqBody</a> a</li></ul></div><div id="interface"><h1>Documentation</h1><div class="top"><p class="src"><span class="keyword">data</span> <a name="t:ReqBody" class="def">ReqBody</a> a <a href="src/Servant-API-ReqBody.html#ReqBody" class="link">Source</a></p><div class="doc"><p>Extract the request body as a value of type <code>a</code>.</p><p>Example:</p><pre> -- POST /books
|
||||
type MyApi = "books" :> ReqBody Book :> Post Book</pre></div><div class="subs instances"><p id="control.i:ReqBody" class="caption collapser" onclick="toggleSection('i:ReqBody')">Instances</p><div id="section.i:ReqBody" class="show"><table><tr><td class="src">(<a href="https://hackage.haskell.org/package/aeson-0.8.0.2/docs/Data-Aeson-Types.html#t:FromJSON">FromJSON</a> a, <a href="Servant-Server.html#t:HasServer">HasServer</a> sublayout) => <a href="Servant-Server.html#t:HasServer">HasServer</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-ReqBody.html#t:ReqBody">ReqBody</a> * a) sublayout)</td><td class="doc"><p>If you use <code><a href="Servant-API-ReqBody.html#t:ReqBody">ReqBody</a></code> in one of the endpoints for your API,
|
||||
this automatically requires your server-side handler to be a function
|
||||
that takes an argument of the type specified by <code><a href="Servant-API-ReqBody.html#t:ReqBody">ReqBody</a></code>.
|
||||
This lets servant worry about extracting it from the request and turning
|
||||
it into a value of the type you specify.</p><p>All it asks is for a <code><a href="https://hackage.haskell.org/package/aeson-0.8.0.2/docs/Data-Aeson-Types.html#t:FromJSON">FromJSON</a></code> instance.</p><p>Example:</p><pre>type MyApi = "books" :> ReqBody Book :> Post Book
|
||||
|
||||
server :: Server MyApi
|
||||
server = postBook
|
||||
where postBook :: Book -> EitherT (Int, String) IO Book
|
||||
postBook book = ...insert into your db...</pre></td></tr><tr><td class="src"><span class="keyword">type</span> <a href="Servant-Server.html#t:Server">Server</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-ReqBody.html#t:ReqBody">ReqBody</a> * a) sublayout) = a -> <a href="Servant-Server.html#t:Server">Server</a> sublayout</td><td class="doc empty"> </td></tr></table></div></div></div></div></div><div id="footer"><p>Produced by <a href="http://www.haskell.org/haddock/">Haddock</a> version 2.15.0</p></div></body></html>
|
69
Servant-API-Sub.html
Normal file
69
Servant-API-Sub.html
Normal file
|
@ -0,0 +1,69 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><title>Servant.API.Sub</title><link href="ocean.css" rel="stylesheet" type="text/css" title="Ocean" /><script src="haddock-util.js" type="text/javascript"></script><script type="text/javascript">//<![CDATA[
|
||||
window.onload = function () {pageLoad();setSynopsis("mini_Servant-API-Sub.html");};
|
||||
//]]>
|
||||
</script></head><body><div id="package-header"><ul class="links" id="page-menu"><li><a href="src/Servant-API-Sub.html">Source</a></li><li><a href="index.html">Contents</a></li><li><a href="doc-index.html">Index</a></li></ul><p class="caption">servant-0.2: A family of combinators for defining webservices APIs and serving them</p></div><div id="content"><div id="module-header"><table class="info"><tr><th>Safe Haskell</th><td>None</td></tr><tr><th>Language</th><td>Haskell2010</td></tr></table><p class="caption">Servant.API.Sub</p></div><div id="synopsis"><p id="control.syn" class="caption expander" onclick="toggleSection('syn')">Synopsis</p><ul id="section.syn" class="hide" onclick="toggleSection('syn')"><li class="src short"><span class="keyword">data</span> path <a href="#t::-62-">:></a> a = (<a href="Servant.html#t:Proxy">Proxy</a> path) <a href="#v::-62-">:></a> a</li></ul></div><div id="interface"><h1>Documentation</h1><div class="top"><p class="src"><span class="keyword">data</span> path <a name="t::-62-" class="def">:></a> a <span class="fixity">infixr 9</span><span class="rightedge"></span> <a href="src/Servant-API-Sub.html#%3A%3E" class="link">Source</a></p><div class="doc"><p>The contained API (second argument) can be found under <code>("/" ++ path)</code>
|
||||
(path being the first argument).</p><p>Example:</p><pre>-- GET /hello/world
|
||||
-- returning a JSON encoded World value
|
||||
type MyApi = "hello" :> "world" :> Get World</pre></div><div class="subs constructors"><p class="caption">Constructors</p><table><tr><td class="src">(<a href="Servant.html#t:Proxy">Proxy</a> path) <a name="v::-62-" class="def">:></a> a <span class="fixity">infixr 9</span><span class="rightedge"></span></td><td class="doc empty"> </td></tr></table></div><div class="subs instances"><p id="control.i::-62-" class="caption collapser" onclick="toggleSection('i::-62-')">Instances</p><div id="section.i::-62-" class="show"><table><tr><td class="src">(<a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:KnownSymbol">KnownSymbol</a> s, <a href="Servant-Utils-Links.html#t:VLinkHelper">VLinkHelper</a> * e) => <a href="Servant-Utils-Links.html#t:VLinkHelper">VLinkHelper</a> * (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:Symbol">Symbol</a> s e)</td><td class="doc empty"> </td></tr><tr><td class="src">(<a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:KnownSymbol">KnownSymbol</a> capture, <a href="Servant-Common-Text.html#t:FromText">FromText</a> a, <a href="Servant-Server.html#t:HasServer">HasServer</a> sublayout) => <a href="Servant-Server.html#t:HasServer">HasServer</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-Capture.html#t:Capture">Capture</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:Symbol">Symbol</a> * capture a) sublayout)</td><td class="doc"><p>If you use <code><a href="Servant-API-Capture.html#t:Capture">Capture</a></code> in one of the endpoints for your API,
|
||||
this automatically requires your server-side handler to be a function
|
||||
that takes an argument of the type specified by the <code><a href="Servant-API-Capture.html#t:Capture">Capture</a></code>.
|
||||
This lets servant worry about getting it from the URL and turning
|
||||
it into a value of the type you specify.</p><p>You can control how it'll be converted from <code><a href="https://hackage.haskell.org/package/text-1.2.0.0/docs/Data-Text.html#t:Text">Text</a></code> to your type
|
||||
by simply providing an instance of <code><a href="Servant-Common-Text.html#t:FromText">FromText</a></code> for your type.</p><p>Example:</p><pre>type MyApi = "books" :> Capture "isbn" Text :> Get Book
|
||||
|
||||
server :: Server MyApi
|
||||
server = getBook
|
||||
where getBook :: Text -> EitherT (Int, String) IO Book
|
||||
getBook isbn = ...</pre></td></tr><tr><td class="src">(<a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:KnownSymbol">KnownSymbol</a> sym, <a href="Servant-Common-Text.html#t:FromText">FromText</a> a, <a href="Servant-Server.html#t:HasServer">HasServer</a> sublayout) => <a href="Servant-Server.html#t:HasServer">HasServer</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-Header.html#t:Header">Header</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:Symbol">Symbol</a> * sym a) sublayout)</td><td class="doc"><p>If you use <code><a href="Servant-API-Header.html#t:Header">Header</a></code> in one of the endpoints for your API,
|
||||
this automatically requires your server-side handler to be a function
|
||||
that takes an argument of the type specified by <code><a href="Servant-API-Header.html#t:Header">Header</a></code>.
|
||||
This lets servant worry about extracting it from the request and turning
|
||||
it into a value of the type you specify.</p><p>All it asks is for a <code><a href="Servant-Common-Text.html#t:FromText">FromText</a></code> instance.</p><p>Example:</p><pre>newtype Referer = Referer Text
|
||||
deriving (Eq, Show, FromText, ToText)
|
||||
|
||||
-- GET /view-my-referer
|
||||
type MyApi = "view-my-referer" :> Header "Referer" Referer :> Get Referer
|
||||
|
||||
server :: Server MyApi
|
||||
server = viewReferer
|
||||
where viewReferer :: Referer -> EitherT (Int, String) IO referer
|
||||
viewReferer referer = return referer</pre></td></tr><tr><td class="src">(<a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:KnownSymbol">KnownSymbol</a> sym, <a href="Servant-Server.html#t:HasServer">HasServer</a> sublayout) => <a href="Servant-Server.html#t:HasServer">HasServer</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-QueryParam.html#t:QueryFlag">QueryFlag</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:Symbol">Symbol</a> sym) sublayout)</td><td class="doc"><p>If you use <code><code><a href="Servant-API-QueryParam.html#t:QueryFlag">QueryFlag</a></code> "published"</code> in one of the endpoints for your API,
|
||||
this automatically requires your server-side handler to be a function
|
||||
that takes an argument of type <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Bool.html#t:Bool">Bool</a></code>.</p><p>Example:</p><pre>type MyApi = "books" :> QueryFlag "published" :> Get [Book]
|
||||
|
||||
server :: Server MyApi
|
||||
server = getBooks
|
||||
where getBooks :: Bool -> EitherT (Int, String) IO [Book]
|
||||
getBooks onlyPublished = ...return all books, or only the ones that are already published, depending on the argument...</pre></td></tr><tr><td class="src">(<a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:KnownSymbol">KnownSymbol</a> sym, <a href="Servant-Common-Text.html#t:FromText">FromText</a> a, <a href="Servant-Server.html#t:HasServer">HasServer</a> sublayout) => <a href="Servant-Server.html#t:HasServer">HasServer</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-QueryParam.html#t:QueryParams">QueryParams</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:Symbol">Symbol</a> * sym a) sublayout)</td><td class="doc"><p>If you use <code><code><a href="Servant-API-QueryParam.html#t:QueryParams">QueryParams</a></code> "authors" Text</code> in one of the endpoints for your API,
|
||||
this automatically requires your server-side handler to be a function
|
||||
that takes an argument of type <code>[<code>Text</code>]</code>.</p><p>This lets servant worry about looking up 0 or more values in the query string
|
||||
associated to <code>authors</code> and turning each of them into a value of
|
||||
the type you specify.</p><p>You can control how the individual values are converted from <code>Text</code> to your type
|
||||
by simply providing an instance of <code><a href="Servant-Common-Text.html#t:FromText">FromText</a></code> for your type.</p><p>Example:</p><pre>type MyApi = "books" :> QueryParams "authors" Text :> Get [Book]
|
||||
|
||||
server :: Server MyApi
|
||||
server = getBooksBy
|
||||
where getBooksBy :: [Text] -> EitherT (Int, String) IO [Book]
|
||||
getBooksBy authors = ...return all books by these authors...</pre></td></tr><tr><td class="src">(<a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:KnownSymbol">KnownSymbol</a> sym, <a href="Servant-Common-Text.html#t:FromText">FromText</a> a, <a href="Servant-Server.html#t:HasServer">HasServer</a> sublayout) => <a href="Servant-Server.html#t:HasServer">HasServer</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-QueryParam.html#t:QueryParam">QueryParam</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:Symbol">Symbol</a> * sym a) sublayout)</td><td class="doc"><p>If you use <code><code><a href="Servant-API-QueryParam.html#t:QueryParam">QueryParam</a></code> "author" Text</code> in one of the endpoints for your API,
|
||||
this automatically requires your server-side handler to be a function
|
||||
that takes an argument of type <code><code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Maybe.html#t:Maybe">Maybe</a></code> <code>Text</code></code>.</p><p>This lets servant worry about looking it up in the query string
|
||||
and turning it into a value of the type you specify, enclosed
|
||||
in <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Maybe.html#t:Maybe">Maybe</a></code>, because it may not be there and servant would then
|
||||
hand you <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Maybe.html#v:Nothing">Nothing</a></code>.</p><p>You can control how it'll be converted from <code>Text</code> to your type
|
||||
by simply providing an instance of <code><a href="Servant-Common-Text.html#t:FromText">FromText</a></code> for your type.</p><p>Example:</p><pre>type MyApi = "books" :> QueryParam "author" Text :> Get [Book]
|
||||
|
||||
server :: Server MyApi
|
||||
server = getBooksBy
|
||||
where getBooksBy :: Maybe Text -> EitherT (Int, String) IO [Book]
|
||||
getBooksBy Nothing = ...return all books...
|
||||
getBooksBy (Just author) = ...return books by the given author...</pre></td></tr><tr><td class="src">(<a href="https://hackage.haskell.org/package/aeson-0.8.0.2/docs/Data-Aeson-Types.html#t:FromJSON">FromJSON</a> a, <a href="Servant-Server.html#t:HasServer">HasServer</a> sublayout) => <a href="Servant-Server.html#t:HasServer">HasServer</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-ReqBody.html#t:ReqBody">ReqBody</a> * a) sublayout)</td><td class="doc"><p>If you use <code><a href="Servant-API-ReqBody.html#t:ReqBody">ReqBody</a></code> in one of the endpoints for your API,
|
||||
this automatically requires your server-side handler to be a function
|
||||
that takes an argument of the type specified by <code><a href="Servant-API-ReqBody.html#t:ReqBody">ReqBody</a></code>.
|
||||
This lets servant worry about extracting it from the request and turning
|
||||
it into a value of the type you specify.</p><p>All it asks is for a <code><a href="https://hackage.haskell.org/package/aeson-0.8.0.2/docs/Data-Aeson-Types.html#t:FromJSON">FromJSON</a></code> instance.</p><p>Example:</p><pre>type MyApi = "books" :> ReqBody Book :> Post Book
|
||||
|
||||
server :: Server MyApi
|
||||
server = postBook
|
||||
where postBook :: Book -> EitherT (Int, String) IO Book
|
||||
postBook book = ...insert into your db...</pre></td></tr><tr><td class="src">(<a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:KnownSymbol">KnownSymbol</a> path, <a href="Servant-Server.html#t:HasServer">HasServer</a> sublayout) => <a href="Servant-Server.html#t:HasServer">HasServer</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:Symbol">Symbol</a> path sublayout)</td><td class="doc"><p>Make sure the incoming request starts with <code>"/path"</code>, strip it and
|
||||
pass the rest of the request path to <code>sublayout</code>.</p></td></tr><tr><td class="src"><span class="keyword">type</span> <a href="Servant-Server.html#t:Server">Server</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-Capture.html#t:Capture">Capture</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:Symbol">Symbol</a> * capture a) sublayout) = a -> <a href="Servant-Server.html#t:Server">Server</a> sublayout</td><td class="doc empty"> </td></tr><tr><td class="src"><span class="keyword">type</span> <a href="Servant-Server.html#t:Server">Server</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-Header.html#t:Header">Header</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:Symbol">Symbol</a> * sym a) sublayout) = <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Maybe.html#t:Maybe">Maybe</a> a -> <a href="Servant-Server.html#t:Server">Server</a> sublayout</td><td class="doc empty"> </td></tr><tr><td class="src"><span class="keyword">type</span> <a href="Servant-Server.html#t:Server">Server</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-QueryParam.html#t:QueryFlag">QueryFlag</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:Symbol">Symbol</a> sym) sublayout) = <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Bool.html#t:Bool">Bool</a> -> <a href="Servant-Server.html#t:Server">Server</a> sublayout</td><td class="doc empty"> </td></tr><tr><td class="src"><span class="keyword">type</span> <a href="Servant-Server.html#t:Server">Server</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-QueryParam.html#t:QueryParams">QueryParams</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:Symbol">Symbol</a> * sym a) sublayout) = [a] -> <a href="Servant-Server.html#t:Server">Server</a> sublayout</td><td class="doc empty"> </td></tr><tr><td class="src"><span class="keyword">type</span> <a href="Servant-Server.html#t:Server">Server</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-QueryParam.html#t:QueryParam">QueryParam</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:Symbol">Symbol</a> * sym a) sublayout) = <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Maybe.html#t:Maybe">Maybe</a> a -> <a href="Servant-Server.html#t:Server">Server</a> sublayout</td><td class="doc empty"> </td></tr><tr><td class="src"><span class="keyword">type</span> <a href="Servant-Server.html#t:Server">Server</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-ReqBody.html#t:ReqBody">ReqBody</a> * a) sublayout) = a -> <a href="Servant-Server.html#t:Server">Server</a> sublayout</td><td class="doc empty"> </td></tr><tr><td class="src"><span class="keyword">type</span> <a href="Servant-Server.html#t:Server">Server</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:Symbol">Symbol</a> path sublayout) = <a href="Servant-Server.html#t:Server">Server</a> sublayout</td><td class="doc empty"> </td></tr></table></div></div></div></div></div><div id="footer"><p>Produced by <a href="http://www.haskell.org/haddock/">Haddock</a> version 2.15.0</p></div></body></html>
|
4
Servant-API.html
Normal file
4
Servant-API.html
Normal file
File diff suppressed because one or more lines are too long
7
Servant-Common-Text.html
Normal file
7
Servant-Common-Text.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><title>Servant.Common.Text</title><link href="ocean.css" rel="stylesheet" type="text/css" title="Ocean" /><script src="haddock-util.js" type="text/javascript"></script><script type="text/javascript">//<![CDATA[
|
||||
window.onload = function () {pageLoad();setSynopsis("mini_Servant-Common-Text.html");};
|
||||
//]]>
|
||||
</script></head><body><div id="package-header"><ul class="links" id="page-menu"><li><a href="src/Servant-Common-Text.html">Source</a></li><li><a href="index.html">Contents</a></li><li><a href="doc-index.html">Index</a></li></ul><p class="caption">servant-0.2: A family of combinators for defining webservices APIs and serving them</p></div><div id="content"><div id="module-header"><table class="info"><tr><th>Safe Haskell</th><td>Safe-Inferred</td></tr><tr><th>Language</th><td>Haskell2010</td></tr></table><p class="caption">Servant.Common.Text</p></div><div id="synopsis"><p id="control.syn" class="caption expander" onclick="toggleSection('syn')">Synopsis</p><ul id="section.syn" class="hide" onclick="toggleSection('syn')"><li class="src short"><span class="keyword">class</span> <a href="#t:FromText">FromText</a> a <span class="keyword">where</span><ul class="subs"><li><a href="#v:fromText">fromText</a> :: <a href="https://hackage.haskell.org/package/text-1.2.0.0/docs/Data-Text.html#t:Text">Text</a> -> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Maybe.html#t:Maybe">Maybe</a> a</li></ul></li><li class="src short"><span class="keyword">class</span> <a href="#t:ToText">ToText</a> a <span class="keyword">where</span><ul class="subs"><li><a href="#v:toText">toText</a> :: a -> <a href="https://hackage.haskell.org/package/text-1.2.0.0/docs/Data-Text.html#t:Text">Text</a></li></ul></li></ul></div><div id="interface"><h1>Documentation</h1><div class="top"><p class="src"><span class="keyword">class</span> <a name="t:FromText" class="def">FromText</a> a <span class="keyword">where</span> <a href="src/Servant-Common-Text.html#FromText" class="link">Source</a></p><div class="doc"><p>For getting values from url captures and query string parameters</p></div><div class="subs methods"><p class="caption">Methods</p><p class="src"><a name="v:fromText" class="def">fromText</a> :: <a href="https://hackage.haskell.org/package/text-1.2.0.0/docs/Data-Text.html#t:Text">Text</a> -> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Maybe.html#t:Maybe">Maybe</a> a <a href="src/Servant-Common-Text.html#fromText" class="link">Source</a></p></div><div class="subs instances"><p id="control.i:FromText" class="caption collapser" onclick="toggleSection('i:FromText')">Instances</p><div id="section.i:FromText" class="show"><table><tr><td class="src"><a href="Servant-Common-Text.html#t:FromText">FromText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Bool.html#t:Bool">Bool</a></td><td class="doc"><pre>fromText "true" = Just True
|
||||
fromText "false" = Just False
|
||||
fromText _ = Nothing</pre></td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:FromText">FromText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Prelude.html#t:Double">Double</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:FromText">FromText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Prelude.html#t:Float">Float</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:FromText">FromText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Int.html#t:Int">Int</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:FromText">FromText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Int.html#t:Int8">Int8</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:FromText">FromText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Int.html#t:Int16">Int16</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:FromText">FromText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Int.html#t:Int32">Int32</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:FromText">FromText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Int.html#t:Int64">Int64</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:FromText">FromText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Prelude.html#t:Integer">Integer</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:FromText">FromText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Word.html#t:Word">Word</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:FromText">FromText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Word.html#t:Word8">Word8</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:FromText">FromText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Word.html#t:Word16">Word16</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:FromText">FromText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Word.html#t:Word32">Word32</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:FromText">FromText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Word.html#t:Word64">Word64</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:FromText">FromText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-String.html#t:String">String</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:FromText">FromText</a> <a href="https://hackage.haskell.org/package/text-1.2.0.0/docs/Data-Text.html#t:Text">Text</a></td><td class="doc empty"> </td></tr></table></div></div></div><div class="top"><p class="src"><span class="keyword">class</span> <a name="t:ToText" class="def">ToText</a> a <span class="keyword">where</span> <a href="src/Servant-Common-Text.html#ToText" class="link">Source</a></p><div class="doc"><p>For putting values in paths and query string parameters</p></div><div class="subs methods"><p class="caption">Methods</p><p class="src"><a name="v:toText" class="def">toText</a> :: a -> <a href="https://hackage.haskell.org/package/text-1.2.0.0/docs/Data-Text.html#t:Text">Text</a> <a href="src/Servant-Common-Text.html#toText" class="link">Source</a></p></div><div class="subs instances"><p id="control.i:ToText" class="caption collapser" onclick="toggleSection('i:ToText')">Instances</p><div id="section.i:ToText" class="show"><table><tr><td class="src"><a href="Servant-Common-Text.html#t:ToText">ToText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Bool.html#t:Bool">Bool</a></td><td class="doc"><pre>toText True = "true"
|
||||
toText False = "false"</pre></td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:ToText">ToText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Prelude.html#t:Double">Double</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:ToText">ToText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Prelude.html#t:Float">Float</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:ToText">ToText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Int.html#t:Int">Int</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:ToText">ToText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Int.html#t:Int8">Int8</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:ToText">ToText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Int.html#t:Int16">Int16</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:ToText">ToText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Int.html#t:Int32">Int32</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:ToText">ToText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Int.html#t:Int64">Int64</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:ToText">ToText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Prelude.html#t:Integer">Integer</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:ToText">ToText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Word.html#t:Word">Word</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:ToText">ToText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Word.html#t:Word8">Word8</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:ToText">ToText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Word.html#t:Word16">Word16</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:ToText">ToText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Word.html#t:Word32">Word32</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:ToText">ToText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Word.html#t:Word64">Word64</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:ToText">ToText</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-String.html#t:String">String</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="Servant-Common-Text.html#t:ToText">ToText</a> <a href="https://hackage.haskell.org/package/text-1.2.0.0/docs/Data-Text.html#t:Text">Text</a></td><td class="doc empty"> </td></tr></table></div></div></div></div></div><div id="footer"><p>Produced by <a href="http://www.haskell.org/haddock/">Haddock</a> version 2.15.0</p></div></body></html>
|
17
Servant-QQ.html
Normal file
17
Servant-QQ.html
Normal file
File diff suppressed because one or more lines are too long
129
Servant-Server.html
Normal file
129
Servant-Server.html
Normal file
|
@ -0,0 +1,129 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><title>Servant.Server</title><link href="ocean.css" rel="stylesheet" type="text/css" title="Ocean" /><script src="haddock-util.js" type="text/javascript"></script><script type="text/javascript">//<![CDATA[
|
||||
window.onload = function () {pageLoad();setSynopsis("mini_Servant-Server.html");};
|
||||
//]]>
|
||||
</script></head><body><div id="package-header"><ul class="links" id="page-menu"><li><a href="src/Servant-Server.html">Source</a></li><li><a href="index.html">Contents</a></li><li><a href="doc-index.html">Index</a></li></ul><p class="caption">servant-0.2: A family of combinators for defining webservices APIs and serving them</p></div><div id="content"><div id="module-header"><table class="info"><tr><th>Safe Haskell</th><td>None</td></tr><tr><th>Language</th><td>Haskell2010</td></tr></table><p class="caption">Servant.Server</p></div><div id="table-of-contents"><p class="caption">Contents</p><ul><li><a href="#g:1">Implementing Servers</a></li><li><a href="#g:2">Route mismatch</a></li></ul></div><div id="description"><p class="caption">Description</p><div class="doc"><p>This module lets you implement <code><a href="Servant-Server.html#t:Server">Server</a></code>s for defined APIs. You'll
|
||||
most likely just need <code><a href="Servant-Server.html#v:serve">serve</a></code>.</p></div></div><div id="synopsis"><p id="control.syn" class="caption expander" onclick="toggleSection('syn')">Synopsis</p><ul id="section.syn" class="hide" onclick="toggleSection('syn')"><li class="src short"><a href="#v:serve">serve</a> :: <a href="Servant-Server.html#t:HasServer">HasServer</a> layout => <a href="Servant.html#t:Proxy">Proxy</a> layout -> <a href="Servant-Server.html#t:Server">Server</a> layout -> <a href="https://hackage.haskell.org/package/wai-3.0.2/docs/Network-Wai.html#t:Application">Application</a></li><li class="src short"><a href="#v:toApplication">toApplication</a> :: <a href="Servant-Server.html#t:RoutingApplication">RoutingApplication</a> -> <a href="https://hackage.haskell.org/package/wai-3.0.2/docs/Network-Wai.html#t:Application">Application</a></li><li class="src short"><span class="keyword">data</span> <a href="#t:RouteMismatch">RouteMismatch</a><ul class="subs"><li>= <a href="#v:NotFound">NotFound</a></li><li>| <a href="#v:WrongMethod">WrongMethod</a></li><li>| <a href="#v:InvalidBody">InvalidBody</a></li></ul></li><li class="src short"><span class="keyword">newtype</span> <a href="#t:RouteResult">RouteResult</a> a = <a href="#v:RR">RR</a> {<ul class="subs"><li><a href="#v:routeResult">routeResult</a> :: <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Either.html#t:Either">Either</a> <a href="Servant-Server.html#t:RouteMismatch">RouteMismatch</a> a</li></ul>}</li><li class="src short"><a href="#v:failWith">failWith</a> :: <a href="Servant-Server.html#t:RouteMismatch">RouteMismatch</a> -> <a href="Servant-Server.html#t:RouteResult">RouteResult</a> a</li><li class="src short"><a href="#v:succeedWith">succeedWith</a> :: a -> <a href="Servant-Server.html#t:RouteResult">RouteResult</a> a</li><li class="src short"><a href="#v:isMismatch">isMismatch</a> :: <a href="Servant-Server.html#t:RouteResult">RouteResult</a> a -> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Bool.html#t:Bool">Bool</a></li><li class="src short"><span class="keyword">type</span> <a href="#t:RoutingApplication">RoutingApplication</a> = <a href="https://hackage.haskell.org/package/wai-3.0.2/docs/Network-Wai.html#t:Request">Request</a> -> (<a href="Servant-Server.html#t:RouteResult">RouteResult</a> <a href="https://hackage.haskell.org/package/wai-3.0.2/docs/Network-Wai.html#t:Response">Response</a> -> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/System-IO.html#t:IO">IO</a> <a href="https://hackage.haskell.org/package/wai-3.0.2/docs/Network-Wai.html#t:ResponseReceived">ResponseReceived</a>) -> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/System-IO.html#t:IO">IO</a> <a href="https://hackage.haskell.org/package/wai-3.0.2/docs/Network-Wai.html#t:ResponseReceived">ResponseReceived</a></li><li class="src short"><span class="keyword">class</span> <a href="#t:HasServer">HasServer</a> layout <span class="keyword">where</span><ul class="subs"><li><span class="keyword">type</span> <a href="#t:Server">Server</a> layout :: *</li><li><a href="#v:route">route</a> :: <a href="Servant.html#t:Proxy">Proxy</a> layout -> <a href="Servant-Server.html#t:Server">Server</a> layout -> <a href="Servant-Server.html#t:RoutingApplication">RoutingApplication</a></li></ul></li></ul></div><div id="interface"><h1 id="g:1">Implementing Servers</h1><div class="top"><p class="src"><a name="v:serve" class="def">serve</a> :: <a href="Servant-Server.html#t:HasServer">HasServer</a> layout => <a href="Servant.html#t:Proxy">Proxy</a> layout -> <a href="Servant-Server.html#t:Server">Server</a> layout -> <a href="https://hackage.haskell.org/package/wai-3.0.2/docs/Network-Wai.html#t:Application">Application</a> <a href="src/Servant-Server.html#serve" class="link">Source</a></p><div class="doc"><p><code><a href="Servant-Server.html#v:serve">serve</a></code> allows you to implement an API and produce a wai <code><a href="https://hackage.haskell.org/package/wai-3.0.2/docs/Network-Wai.html#t:Application">Application</a></code>.</p><p>Example:</p><pre>type MyApi = "books" :> Get [Book] -- GET /books
|
||||
:<|> "books" :> ReqBody Book :> Post Book -- POST /books
|
||||
|
||||
server :: Server MyApi
|
||||
server = listAllBooks :<|> postBook
|
||||
where listAllBooks = ...
|
||||
postBook book = ...
|
||||
|
||||
app :: Application
|
||||
app = serve myApi server
|
||||
|
||||
main :: IO ()
|
||||
main = Network.Wai.Handler.Warp.run 8080 app</pre></div></div><div class="top"><p class="src"><a name="v:toApplication" class="def">toApplication</a> :: <a href="Servant-Server.html#t:RoutingApplication">RoutingApplication</a> -> <a href="https://hackage.haskell.org/package/wai-3.0.2/docs/Network-Wai.html#t:Application">Application</a> <a href="src/Servant-Server.html#toApplication" class="link">Source</a></p></div><h1 id="g:2">Route mismatch</h1><div class="top"><p class="src"><span class="keyword">data</span> <a name="t:RouteMismatch" class="def">RouteMismatch</a> <a href="src/Servant-Server.html#RouteMismatch" class="link">Source</a></p><div class="subs constructors"><p class="caption">Constructors</p><table><tr><td class="src"><a name="v:NotFound" class="def">NotFound</a></td><td class="doc"><p>the usual "not found" error</p></td></tr><tr><td class="src"><a name="v:WrongMethod" class="def">WrongMethod</a></td><td class="doc"><p>a more informative "you just got the HTTP method wrong" error</p></td></tr><tr><td class="src"><a name="v:InvalidBody" class="def">InvalidBody</a></td><td class="doc"><p>an even more informative "your json request body wasn't valid" error</p></td></tr></table></div><div class="subs instances"><p id="control.i:RouteMismatch" class="caption collapser" onclick="toggleSection('i:RouteMismatch')">Instances</p><div id="section.i:RouteMismatch" class="show"><table><tr><td class="src"><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Eq.html#t:Eq">Eq</a> <a href="Servant-Server.html#t:RouteMismatch">RouteMismatch</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Text-Show.html#t:Show">Show</a> <a href="Servant-Server.html#t:RouteMismatch">RouteMismatch</a></td><td class="doc empty"> </td></tr><tr><td class="src"><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Monoid.html#t:Monoid">Monoid</a> <a href="Servant-Server.html#t:RouteMismatch">RouteMismatch</a></td><td class="doc"><pre>> mempty = NotFound
|
||||
>
|
||||
> NotFound <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Monoid.html#v:mappend">mappend</a></code> x = x
|
||||
> WrongMethod <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Monoid.html#v:mappend">mappend</a></code> InvalidBody = InvalidBody
|
||||
> WrongMethod <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Monoid.html#v:mappend">mappend</a></code> _ = WrongMethod
|
||||
> InvalidBody <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Monoid.html#v:mappend">mappend</a></code> _ = InvalidBody
|
||||
</pre></td></tr></table></div></div></div><div class="top"><p class="src"><span class="keyword">newtype</span> <a name="t:RouteResult" class="def">RouteResult</a> a <a href="src/Servant-Server.html#RouteResult" class="link">Source</a></p><div class="doc"><p>A wrapper around <code><code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Either.html#t:Either">Either</a></code> <code><a href="Servant-Server.html#t:RouteMismatch">RouteMismatch</a></code> a</code>.</p></div><div class="subs constructors"><p class="caption">Constructors</p><table><tr><td class="src"><a name="v:RR" class="def">RR</a></td><td class="doc empty"> </td></tr><tr><td colspan="2"><div class="subs fields"><p class="caption">Fields</p><dl><dt class="src"><a name="v:routeResult" class="def">routeResult</a> :: <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Either.html#t:Either">Either</a> <a href="Servant-Server.html#t:RouteMismatch">RouteMismatch</a> a</dt><dd class="doc empty"> </dd></dl><div class="clear"></div></div></td></tr></table></div><div class="subs instances"><p id="control.i:RouteResult" class="caption collapser" onclick="toggleSection('i:RouteResult')">Instances</p><div id="section.i:RouteResult" class="show"><table><tr><td class="src"><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Eq.html#t:Eq">Eq</a> a => <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Eq.html#t:Eq">Eq</a> (<a href="Servant-Server.html#t:RouteResult">RouteResult</a> a)</td><td class="doc empty"> </td></tr><tr><td class="src"><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Text-Show.html#t:Show">Show</a> a => <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Text-Show.html#t:Show">Show</a> (<a href="Servant-Server.html#t:RouteResult">RouteResult</a> a)</td><td class="doc empty"> </td></tr><tr><td class="src"><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Monoid.html#t:Monoid">Monoid</a> (<a href="Servant-Server.html#t:RouteResult">RouteResult</a> a)</td><td class="doc"><p>If we get a <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Either.html#v:Right">Right</a></code>, it has precedence over everything else.</p><p>This in particular means that if we could get several <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Either.html#v:Right">Right</a></code>s,
|
||||
only the first we encounter would be taken into account.</p></td></tr></table></div></div></div><div class="top"><p class="src"><a name="v:failWith" class="def">failWith</a> :: <a href="Servant-Server.html#t:RouteMismatch">RouteMismatch</a> -> <a href="Servant-Server.html#t:RouteResult">RouteResult</a> a <a href="src/Servant-Server.html#failWith" class="link">Source</a></p></div><div class="top"><p class="src"><a name="v:succeedWith" class="def">succeedWith</a> :: a -> <a href="Servant-Server.html#t:RouteResult">RouteResult</a> a <a href="src/Servant-Server.html#succeedWith" class="link">Source</a></p></div><div class="top"><p class="src"><a name="v:isMismatch" class="def">isMismatch</a> :: <a href="Servant-Server.html#t:RouteResult">RouteResult</a> a -> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Bool.html#t:Bool">Bool</a> <a href="src/Servant-Server.html#isMismatch" class="link">Source</a></p></div><div class="top"><p class="src"><span class="keyword">type</span> <a name="t:RoutingApplication" class="def">RoutingApplication</a> <a href="src/Servant-Server.html#RoutingApplication" class="link">Source</a></p><div class="subs arguments"><p class="caption">Arguments</p><table><tr><td class="src"> = <a href="https://hackage.haskell.org/package/wai-3.0.2/docs/Network-Wai.html#t:Request">Request</a></td><td class="doc"><p>the request, the field <code><a href="https://hackage.haskell.org/package/wai-3.0.2/docs/Network-Wai.html#v:pathInfo">pathInfo</a></code> may be modified by url routing</p></td></tr><tr><td class="src">-> (<a href="Servant-Server.html#t:RouteResult">RouteResult</a> <a href="https://hackage.haskell.org/package/wai-3.0.2/docs/Network-Wai.html#t:Response">Response</a> -> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/System-IO.html#t:IO">IO</a> <a href="https://hackage.haskell.org/package/wai-3.0.2/docs/Network-Wai.html#t:ResponseReceived">ResponseReceived</a>)</td><td class="doc empty"> </td></tr><tr><td class="src">-> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/System-IO.html#t:IO">IO</a> <a href="https://hackage.haskell.org/package/wai-3.0.2/docs/Network-Wai.html#t:ResponseReceived">ResponseReceived</a></td><td class="doc empty"> </td></tr></table></div></div><div class="top"><p class="src"><span class="keyword">class</span> <a name="t:HasServer" class="def">HasServer</a> layout <span class="keyword">where</span> <a href="src/Servant-Server.html#HasServer" class="link">Source</a></p><div class="subs associated-types"><p class="caption">Associated Types</p><p class="src"><span class="keyword">type</span> <a name="t:Server" class="def">Server</a> layout :: * <a href="src/Servant-Server.html#Server" class="link">Source</a></p></div><div class="subs methods"><p class="caption">Methods</p><p class="src"><a name="v:route" class="def">route</a> :: <a href="Servant.html#t:Proxy">Proxy</a> layout -> <a href="Servant-Server.html#t:Server">Server</a> layout -> <a href="Servant-Server.html#t:RoutingApplication">RoutingApplication</a> <a href="src/Servant-Server.html#route" class="link">Source</a></p></div><div class="subs instances"><p id="control.i:HasServer" class="caption collapser" onclick="toggleSection('i:HasServer')">Instances</p><div id="section.i:HasServer" class="show"><table><tr><td class="src"><a href="Servant-Server.html#t:HasServer">HasServer</a> <a href="Servant-API-Delete.html#t:Delete">Delete</a></td><td class="doc"><p>If you have a <code><a href="Servant-API-Delete.html#t:Delete">Delete</a></code> endpoint in your API,
|
||||
the handler for this endpoint is meant to delete
|
||||
a resource.</p><p>The code of the handler will, just like
|
||||
for <code><a href="Servant-API-Get.html#t:Get">Get</a></code>, <code><a href="Servant-API-Post.html#t:Post">Post</a></code> and
|
||||
<code><a href="Servant-API-Put.html#t:Put">Put</a></code>, run in <code>EitherT (Int, String) IO ()</code>.
|
||||
The <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Int.html#t:Int">Int</a></code> represents the status code and the <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-String.html#t:String">String</a></code> a message
|
||||
to be returned. You can use <code><a href="https://hackage.haskell.org/package/either-4.3.2/docs/Control-Monad-Trans-Either.html#v:left">left</a></code> to
|
||||
painlessly error out if the conditions for a successful deletion
|
||||
are not met.</p></td></tr><tr><td class="src"><a href="Servant-Server.html#t:HasServer">HasServer</a> <a href="Servant-API-Raw.html#t:Raw">Raw</a></td><td class="doc"><p>Just pass the request to the underlying application and serve its response.</p><p>Example:</p><pre>type MyApi = "images" :> Raw
|
||||
|
||||
server :: Server MyApi
|
||||
server = serveDirectory "/var/www/images"</pre></td></tr><tr><td class="src"><a href="https://hackage.haskell.org/package/aeson-0.8.0.2/docs/Data-Aeson-Types.html#t:ToJSON">ToJSON</a> result => <a href="Servant-Server.html#t:HasServer">HasServer</a> (<a href="Servant-API-Get.html#t:Get">Get</a> result)</td><td class="doc"><p>When implementing the handler for a <code><a href="Servant-API-Get.html#t:Get">Get</a></code> endpoint,
|
||||
just like for <code><a href="Servant-API-Delete.html#t:Delete">Delete</a></code>, <code><a href="Servant-API-Post.html#t:Post">Post</a></code>
|
||||
and <code><a href="Servant-API-Put.html#t:Put">Put</a></code>, the handler code runs in the
|
||||
<code>EitherT (Int, String) IO</code> monad, where the <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Int.html#t:Int">Int</a></code> represents
|
||||
the status code and the <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-String.html#t:String">String</a></code> a message, returned in case of
|
||||
failure. You can quite handily use <code><a href="Control-Monad-Trans-EitherT.html#v:left">left</a></code>
|
||||
to quickly fail if some conditions are not met.</p><p>If successfully returning a value, we just require that its type has
|
||||
a <code><a href="https://hackage.haskell.org/package/aeson-0.8.0.2/docs/Data-Aeson-Types.html#t:ToJSON">ToJSON</a></code> instance and servant takes care of encoding it for you,
|
||||
yielding status code 200 along the way.</p></td></tr><tr><td class="src"><a href="https://hackage.haskell.org/package/aeson-0.8.0.2/docs/Data-Aeson-Types.html#t:ToJSON">ToJSON</a> a => <a href="Servant-Server.html#t:HasServer">HasServer</a> (<a href="Servant-API-Post.html#t:Post">Post</a> a)</td><td class="doc"><p>When implementing the handler for a <code><a href="Servant-API-Post.html#t:Post">Post</a></code> endpoint,
|
||||
just like for <code><a href="Servant-API-Delete.html#t:Delete">Delete</a></code>, <code><a href="Servant-API-Get.html#t:Get">Get</a></code>
|
||||
and <code><a href="Servant-API-Put.html#t:Put">Put</a></code>, the handler code runs in the
|
||||
<code>EitherT (Int, String) IO</code> monad, where the <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Int.html#t:Int">Int</a></code> represents
|
||||
the status code and the <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-String.html#t:String">String</a></code> a message, returned in case of
|
||||
failure. You can quite handily use <code><a href="Control-Monad-Trans-EitherT.html#v:left">left</a></code>
|
||||
to quickly fail if some conditions are not met.</p><p>If successfully returning a value, we just require that its type has
|
||||
a <code><a href="https://hackage.haskell.org/package/aeson-0.8.0.2/docs/Data-Aeson-Types.html#t:ToJSON">ToJSON</a></code> instance and servant takes care of encoding it for you,
|
||||
yielding status code 201 along the way.</p></td></tr><tr><td class="src"><a href="https://hackage.haskell.org/package/aeson-0.8.0.2/docs/Data-Aeson-Types.html#t:ToJSON">ToJSON</a> a => <a href="Servant-Server.html#t:HasServer">HasServer</a> (<a href="Servant-API-Put.html#t:Put">Put</a> a)</td><td class="doc"><p>When implementing the handler for a <code><a href="Servant-API-Put.html#t:Put">Put</a></code> endpoint,
|
||||
just like for <code><a href="Servant-API-Delete.html#t:Delete">Delete</a></code>, <code><a href="Servant-API-Get.html#t:Get">Get</a></code>
|
||||
and <code><a href="Servant-API-Post.html#t:Post">Post</a></code>, the handler code runs in the
|
||||
<code>EitherT (Int, String) IO</code> monad, where the <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Int.html#t:Int">Int</a></code> represents
|
||||
the status code and the <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-String.html#t:String">String</a></code> a message, returned in case of
|
||||
failure. You can quite handily use <code><a href="Control-Monad-Trans-EitherT.html#v:left">left</a></code>
|
||||
to quickly fail if some conditions are not met.</p><p>If successfully returning a value, we just require that its type has
|
||||
a <code><a href="https://hackage.haskell.org/package/aeson-0.8.0.2/docs/Data-Aeson-Types.html#t:ToJSON">ToJSON</a></code> instance and servant takes care of encoding it for you,
|
||||
yielding status code 200 along the way.</p></td></tr><tr><td class="src">(<a href="Servant-Server.html#t:HasServer">HasServer</a> a, <a href="Servant-Server.html#t:HasServer">HasServer</a> b) => <a href="Servant-Server.html#t:HasServer">HasServer</a> (<a href="Servant-API-Alternative.html#t::-60--124--62-">(:<|>)</a> a b)</td><td class="doc"><p>A server for <code>a <code><a href="Servant-API-Alternative.html#t::-60--124--62-">:<|></a></code> b</code> first tries to match the request again the route
|
||||
represented by <code>a</code> and if it fails tries <code>b</code>. You must provide a request
|
||||
handler for each route.</p><pre>type MyApi = "books" :> Get [Book] -- GET /books
|
||||
:<|> "books" :> ReqBody Book :> Post Book -- POST /books
|
||||
|
||||
server :: Server MyApi
|
||||
server = listAllBooks :<|> postBook
|
||||
where listAllBooks = ...
|
||||
postBook book = ...</pre></td></tr><tr><td class="src">(<a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:KnownSymbol">KnownSymbol</a> capture, <a href="Servant-Common-Text.html#t:FromText">FromText</a> a, <a href="Servant-Server.html#t:HasServer">HasServer</a> sublayout) => <a href="Servant-Server.html#t:HasServer">HasServer</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-Capture.html#t:Capture">Capture</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:Symbol">Symbol</a> * capture a) sublayout)</td><td class="doc"><p>If you use <code><a href="Servant-API-Capture.html#t:Capture">Capture</a></code> in one of the endpoints for your API,
|
||||
this automatically requires your server-side handler to be a function
|
||||
that takes an argument of the type specified by the <code><a href="Servant-API-Capture.html#t:Capture">Capture</a></code>.
|
||||
This lets servant worry about getting it from the URL and turning
|
||||
it into a value of the type you specify.</p><p>You can control how it'll be converted from <code><a href="https://hackage.haskell.org/package/text-1.2.0.0/docs/Data-Text.html#t:Text">Text</a></code> to your type
|
||||
by simply providing an instance of <code><a href="Servant-Common-Text.html#t:FromText">FromText</a></code> for your type.</p><p>Example:</p><pre>type MyApi = "books" :> Capture "isbn" Text :> Get Book
|
||||
|
||||
server :: Server MyApi
|
||||
server = getBook
|
||||
where getBook :: Text -> EitherT (Int, String) IO Book
|
||||
getBook isbn = ...</pre></td></tr><tr><td class="src">(<a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:KnownSymbol">KnownSymbol</a> sym, <a href="Servant-Common-Text.html#t:FromText">FromText</a> a, <a href="Servant-Server.html#t:HasServer">HasServer</a> sublayout) => <a href="Servant-Server.html#t:HasServer">HasServer</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-Header.html#t:Header">Header</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:Symbol">Symbol</a> * sym a) sublayout)</td><td class="doc"><p>If you use <code><a href="Servant-API-Header.html#t:Header">Header</a></code> in one of the endpoints for your API,
|
||||
this automatically requires your server-side handler to be a function
|
||||
that takes an argument of the type specified by <code><a href="Servant-API-Header.html#t:Header">Header</a></code>.
|
||||
This lets servant worry about extracting it from the request and turning
|
||||
it into a value of the type you specify.</p><p>All it asks is for a <code><a href="Servant-Common-Text.html#t:FromText">FromText</a></code> instance.</p><p>Example:</p><pre>newtype Referer = Referer Text
|
||||
deriving (Eq, Show, FromText, ToText)
|
||||
|
||||
-- GET /view-my-referer
|
||||
type MyApi = "view-my-referer" :> Header "Referer" Referer :> Get Referer
|
||||
|
||||
server :: Server MyApi
|
||||
server = viewReferer
|
||||
where viewReferer :: Referer -> EitherT (Int, String) IO referer
|
||||
viewReferer referer = return referer</pre></td></tr><tr><td class="src">(<a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:KnownSymbol">KnownSymbol</a> sym, <a href="Servant-Server.html#t:HasServer">HasServer</a> sublayout) => <a href="Servant-Server.html#t:HasServer">HasServer</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-QueryParam.html#t:QueryFlag">QueryFlag</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:Symbol">Symbol</a> sym) sublayout)</td><td class="doc"><p>If you use <code><code><a href="Servant-API-QueryParam.html#t:QueryFlag">QueryFlag</a></code> "published"</code> in one of the endpoints for your API,
|
||||
this automatically requires your server-side handler to be a function
|
||||
that takes an argument of type <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Bool.html#t:Bool">Bool</a></code>.</p><p>Example:</p><pre>type MyApi = "books" :> QueryFlag "published" :> Get [Book]
|
||||
|
||||
server :: Server MyApi
|
||||
server = getBooks
|
||||
where getBooks :: Bool -> EitherT (Int, String) IO [Book]
|
||||
getBooks onlyPublished = ...return all books, or only the ones that are already published, depending on the argument...</pre></td></tr><tr><td class="src">(<a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:KnownSymbol">KnownSymbol</a> sym, <a href="Servant-Common-Text.html#t:FromText">FromText</a> a, <a href="Servant-Server.html#t:HasServer">HasServer</a> sublayout) => <a href="Servant-Server.html#t:HasServer">HasServer</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-QueryParam.html#t:QueryParams">QueryParams</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:Symbol">Symbol</a> * sym a) sublayout)</td><td class="doc"><p>If you use <code><code><a href="Servant-API-QueryParam.html#t:QueryParams">QueryParams</a></code> "authors" Text</code> in one of the endpoints for your API,
|
||||
this automatically requires your server-side handler to be a function
|
||||
that takes an argument of type <code>[<code>Text</code>]</code>.</p><p>This lets servant worry about looking up 0 or more values in the query string
|
||||
associated to <code>authors</code> and turning each of them into a value of
|
||||
the type you specify.</p><p>You can control how the individual values are converted from <code>Text</code> to your type
|
||||
by simply providing an instance of <code><a href="Servant-Common-Text.html#t:FromText">FromText</a></code> for your type.</p><p>Example:</p><pre>type MyApi = "books" :> QueryParams "authors" Text :> Get [Book]
|
||||
|
||||
server :: Server MyApi
|
||||
server = getBooksBy
|
||||
where getBooksBy :: [Text] -> EitherT (Int, String) IO [Book]
|
||||
getBooksBy authors = ...return all books by these authors...</pre></td></tr><tr><td class="src">(<a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:KnownSymbol">KnownSymbol</a> sym, <a href="Servant-Common-Text.html#t:FromText">FromText</a> a, <a href="Servant-Server.html#t:HasServer">HasServer</a> sublayout) => <a href="Servant-Server.html#t:HasServer">HasServer</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-QueryParam.html#t:QueryParam">QueryParam</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:Symbol">Symbol</a> * sym a) sublayout)</td><td class="doc"><p>If you use <code><code><a href="Servant-API-QueryParam.html#t:QueryParam">QueryParam</a></code> "author" Text</code> in one of the endpoints for your API,
|
||||
this automatically requires your server-side handler to be a function
|
||||
that takes an argument of type <code><code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Maybe.html#t:Maybe">Maybe</a></code> <code>Text</code></code>.</p><p>This lets servant worry about looking it up in the query string
|
||||
and turning it into a value of the type you specify, enclosed
|
||||
in <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Maybe.html#t:Maybe">Maybe</a></code>, because it may not be there and servant would then
|
||||
hand you <code><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Maybe.html#v:Nothing">Nothing</a></code>.</p><p>You can control how it'll be converted from <code>Text</code> to your type
|
||||
by simply providing an instance of <code><a href="Servant-Common-Text.html#t:FromText">FromText</a></code> for your type.</p><p>Example:</p><pre>type MyApi = "books" :> QueryParam "author" Text :> Get [Book]
|
||||
|
||||
server :: Server MyApi
|
||||
server = getBooksBy
|
||||
where getBooksBy :: Maybe Text -> EitherT (Int, String) IO [Book]
|
||||
getBooksBy Nothing = ...return all books...
|
||||
getBooksBy (Just author) = ...return books by the given author...</pre></td></tr><tr><td class="src">(<a href="https://hackage.haskell.org/package/aeson-0.8.0.2/docs/Data-Aeson-Types.html#t:FromJSON">FromJSON</a> a, <a href="Servant-Server.html#t:HasServer">HasServer</a> sublayout) => <a href="Servant-Server.html#t:HasServer">HasServer</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> * (<a href="Servant-API-ReqBody.html#t:ReqBody">ReqBody</a> * a) sublayout)</td><td class="doc"><p>If you use <code><a href="Servant-API-ReqBody.html#t:ReqBody">ReqBody</a></code> in one of the endpoints for your API,
|
||||
this automatically requires your server-side handler to be a function
|
||||
that takes an argument of the type specified by <code><a href="Servant-API-ReqBody.html#t:ReqBody">ReqBody</a></code>.
|
||||
This lets servant worry about extracting it from the request and turning
|
||||
it into a value of the type you specify.</p><p>All it asks is for a <code><a href="https://hackage.haskell.org/package/aeson-0.8.0.2/docs/Data-Aeson-Types.html#t:FromJSON">FromJSON</a></code> instance.</p><p>Example:</p><pre>type MyApi = "books" :> ReqBody Book :> Post Book
|
||||
|
||||
server :: Server MyApi
|
||||
server = postBook
|
||||
where postBook :: Book -> EitherT (Int, String) IO Book
|
||||
postBook book = ...insert into your db...</pre></td></tr><tr><td class="src">(<a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:KnownSymbol">KnownSymbol</a> path, <a href="Servant-Server.html#t:HasServer">HasServer</a> sublayout) => <a href="Servant-Server.html#t:HasServer">HasServer</a> (<a href="Servant-API-Sub.html#t::-62-">(:>)</a> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-TypeLits.html#t:Symbol">Symbol</a> path sublayout)</td><td class="doc"><p>Make sure the incoming request starts with <code>"/path"</code>, strip it and
|
||||
pass the rest of the request path to <code>sublayout</code>.</p></td></tr></table></div></div></div></div></div><div id="footer"><p>Produced by <a href="http://www.haskell.org/haddock/">Haddock</a> version 2.15.0</p></div></body></html>
|
23
Servant-Utils-Links.html
Normal file
23
Servant-Utils-Links.html
Normal file
File diff suppressed because one or more lines are too long
15
Servant-Utils-StaticFiles.html
Normal file
15
Servant-Utils-StaticFiles.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><title>Servant.Utils.StaticFiles</title><link href="ocean.css" rel="stylesheet" type="text/css" title="Ocean" /><script src="haddock-util.js" type="text/javascript"></script><script type="text/javascript">//<![CDATA[
|
||||
window.onload = function () {pageLoad();setSynopsis("mini_Servant-Utils-StaticFiles.html");};
|
||||
//]]>
|
||||
</script></head><body><div id="package-header"><ul class="links" id="page-menu"><li><a href="src/Servant-Utils-StaticFiles.html">Source</a></li><li><a href="index.html">Contents</a></li><li><a href="doc-index.html">Index</a></li></ul><p class="caption">servant-0.2: A family of combinators for defining webservices APIs and serving them</p></div><div id="content"><div id="module-header"><table class="info"><tr><th>Safe Haskell</th><td>None</td></tr><tr><th>Language</th><td>Haskell2010</td></tr></table><p class="caption">Servant.Utils.StaticFiles</p></div><div id="description"><p class="caption">Description</p><div class="doc"><p>This module defines a sever-side handler that lets you serve static files.</p><ul><li><code><a href="Servant-Utils-StaticFiles.html#v:serveDirectory">serveDirectory</a></code> lets you serve anything that lives under a particular
|
||||
directory on your filesystem.</li></ul></div></div><div id="synopsis"><p id="control.syn" class="caption expander" onclick="toggleSection('syn')">Synopsis</p><ul id="section.syn" class="hide" onclick="toggleSection('syn')"><li class="src short"><a href="#v:serveDirectory">serveDirectory</a> :: <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/System-IO.html#t:FilePath">FilePath</a> -> <a href="Servant-Server.html#t:Server">Server</a> <a href="Servant-API-Raw.html#t:Raw">Raw</a></li></ul></div><div id="interface"><h1>Documentation</h1><div class="top"><p class="src"><a name="v:serveDirectory" class="def">serveDirectory</a> :: <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/System-IO.html#t:FilePath">FilePath</a> -> <a href="Servant-Server.html#t:Server">Server</a> <a href="Servant-API-Raw.html#t:Raw">Raw</a> <a href="src/Servant-Utils-StaticFiles.html#serveDirectory" class="link">Source</a></p><div class="doc"><p>Serve anything under the specified directory as a <code><a href="Servant-API-Raw.html#t:Raw">Raw</a></code> endpoint.</p><pre>type MyApi = "static" :> Raw
|
||||
|
||||
server :: Server MyApi
|
||||
server = serveDirectory "/var/www"
|
||||
</pre><p>would capture any request to <code>/static/<something></code> and look for
|
||||
<code><something></code> under <code>/var/www</code>.</p><p>It will do its best to guess the MIME type for that file, based on the extension,
|
||||
and send an appropriate <em>Content-Type</em> header if possible.</p><p>If your goal is to serve HTML, CSS and Javascript files that use the rest of the API
|
||||
as a webapp backend, you will most likely not want the static files to be hidden
|
||||
behind a <em>/static/</em> prefix. In that case, remember to put the <code><a href="Servant-Utils-StaticFiles.html#v:serveDirectory">serveDirectory</a></code>
|
||||
handler in the last position, because <em>servant</em> will try to match the handlers
|
||||
in order.</p></div></div></div></div><div id="footer"><p>Produced by <a href="http://www.haskell.org/haddock/">Haddock</a> version 2.15.0</p></div></body></html>
|
6
Servant.html
Normal file
6
Servant.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><title>Servant</title><link href="ocean.css" rel="stylesheet" type="text/css" title="Ocean" /><script src="haddock-util.js" type="text/javascript"></script><script type="text/javascript">//<![CDATA[
|
||||
window.onload = function () {pageLoad();setSynopsis("mini_Servant.html");};
|
||||
//]]>
|
||||
</script></head><body><div id="package-header"><ul class="links" id="page-menu"><li><a href="src/Servant.html">Source</a></li><li><a href="index.html">Contents</a></li><li><a href="doc-index.html">Index</a></li></ul><p class="caption">servant-0.2: A family of combinators for defining webservices APIs and serving them</p></div><div id="content"><div id="module-header"><table class="info"><tr><th>Safe Haskell</th><td>None</td></tr><tr><th>Language</th><td>Haskell2010</td></tr></table><p class="caption">Servant</p></div><div id="synopsis"><p id="control.syn" class="caption expander" onclick="toggleSection('syn')">Synopsis</p><ul id="section.syn" class="hide" onclick="toggleSection('syn')"><li class="src short">module <a href="Servant-API.html">Servant.API</a></li><li class="src short">module <a href="Servant-Server.html">Servant.Server</a></li><li class="src short">module <a href="Servant-Common-Text.html">Servant.Common.Text</a></li><li class="src short">module <a href="Servant-QQ.html">Servant.QQ</a></li><li class="src short">module <a href="Servant-Utils-Links.html">Servant.Utils.Links</a></li><li class="src short">module <a href="Servant-Utils-StaticFiles.html">Servant.Utils.StaticFiles</a></li><li class="src short"><span class="keyword">data</span> <a href="#t:Proxy">Proxy</a> t :: k -> * = <a href="#v:Proxy">Proxy</a></li></ul></div><div id="interface"><h1>Documentation</h1><div class="doc"><p>This module and its submodules can be used to define servant APIs. Note
|
||||
that these API definitions don't directly implement a server (or anything
|
||||
else).</p></div><div class="top"><p class="src">module <a href="Servant-API.html">Servant.API</a></p></div><div class="doc"><p>For implementing servers for servant APIs.</p></div><div class="top"><p class="src">module <a href="Servant-Server.html">Servant.Server</a></p></div><div class="doc"><p>Using your types in request paths and query string parameters</p></div><div class="top"><p class="src">module <a href="Servant-Common-Text.html">Servant.Common.Text</a></p></div><div class="doc"><p>Utilities on top of the servant core</p></div><div class="top"><p class="src">module <a href="Servant-QQ.html">Servant.QQ</a></p></div><div class="top"><p class="src">module <a href="Servant-Utils-Links.html">Servant.Utils.Links</a></p></div><div class="top"><p class="src">module <a href="Servant-Utils-StaticFiles.html">Servant.Utils.StaticFiles</a></p></div><div class="doc"><p>Useful re-exports</p></div><div class="top"><p class="src"><span class="keyword">data</span> <a name="t:Proxy" class="def">Proxy</a> t :: k -> *</p><div class="doc"><p>A concrete, poly-kinded proxy type</p></div><div class="subs constructors"><p class="caption">Constructors</p><table><tr><td class="src"><a name="v:Proxy" class="def">Proxy</a></td><td class="doc empty"> </td></tr></table></div><div class="subs instances"><p id="control.i:Proxy" class="caption collapser" onclick="toggleSection('i:Proxy')">Instances</p><div id="section.i:Proxy" class="show"><table><tr><td class="src"><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Control-Monad.html#t:Monad">Monad</a> (<a href="Servant.html#t:Proxy">Proxy</a> *)</td><td class="doc empty"> </td></tr><tr><td class="src"><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Control-Monad.html#t:Functor">Functor</a> (<a href="Servant.html#t:Proxy">Proxy</a> *)</td><td class="doc empty"> </td></tr><tr><td class="src"><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Control-Applicative.html#t:Applicative">Applicative</a> (<a href="Servant.html#t:Proxy">Proxy</a> *)</td><td class="doc empty"> </td></tr><tr><td class="src"><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Prelude.html#t:Bounded">Bounded</a> (<a href="Servant.html#t:Proxy">Proxy</a> k s)</td><td class="doc empty"> </td></tr><tr><td class="src"><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Prelude.html#t:Enum">Enum</a> (<a href="Servant.html#t:Proxy">Proxy</a> k s)</td><td class="doc empty"> </td></tr><tr><td class="src"><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Eq.html#t:Eq">Eq</a> (<a href="Servant.html#t:Proxy">Proxy</a> k s)</td><td class="doc empty"> </td></tr><tr><td class="src"><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Ord.html#t:Ord">Ord</a> (<a href="Servant.html#t:Proxy">Proxy</a> k s)</td><td class="doc empty"> </td></tr><tr><td class="src"><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Text-Read.html#t:Read">Read</a> (<a href="Servant.html#t:Proxy">Proxy</a> k s)</td><td class="doc empty"> </td></tr><tr><td class="src"><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Text-Show.html#t:Show">Show</a> (<a href="Servant.html#t:Proxy">Proxy</a> k s)</td><td class="doc empty"> </td></tr><tr><td class="src"><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Ix.html#t:Ix">Ix</a> (<a href="Servant.html#t:Proxy">Proxy</a> k s)</td><td class="doc empty"> </td></tr><tr><td class="src"><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-Generics.html#t:Generic">Generic</a> (<a href="Servant.html#t:Proxy">Proxy</a> * t)</td><td class="doc empty"> </td></tr><tr><td class="src"><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Monoid.html#t:Monoid">Monoid</a> (<a href="Servant.html#t:Proxy">Proxy</a> * s)</td><td class="doc empty"> </td></tr><tr><td class="src"><a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Typeable-Internal.html#t:Typeable">Typeable</a> (k -> *) (<a href="Servant.html#t:Proxy">Proxy</a> k)</td><td class="doc empty"> </td></tr><tr><td class="src"><span class="keyword">type</span> <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-Generics.html#t:Rep">Rep</a> (<a href="Servant.html#t:Proxy">Proxy</a> k t) = <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-Generics.html#t:D1">D1</a> D1Proxy (<a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-Generics.html#t:C1">C1</a> C1_0Proxy <a href="https://hackage.haskell.org/package/base-4.7.0.1/docs/GHC-Generics.html#t:U1">U1</a>)</td><td class="doc empty"> </td></tr></table></div></div></div></div></div><div id="footer"><p>Produced by <a href="http://www.haskell.org/haddock/">Haddock</a> version 2.15.0</p></div></body></html>
|
|
@ -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
|
|
@ -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
|
|
@ -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.
|
||||
}
|
|
@ -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.
|
||||
}
|
|
@ -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.
|
||||
|
||||
}
|
|
@ -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.
|
||||
|
||||
}
|
|
@ -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)
|
||||
```
|
||||
}
|
|
@ -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.
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
synopsis: Add API docs for ServerT
|
||||
prs: #1573
|
|
@ -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.
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
synopsis: Only include question mark for nonempty query strings
|
||||
prs: 1589
|
|
@ -1,2 +0,0 @@
|
|||
synopsis: Run ClientEnv's makeClientRequest in IO.
|
||||
prs: #1595
|
|
@ -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.
|
||||
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
synopsis: Add Functor instance to AuthHandler.
|
||||
prs: #1638
|
|
@ -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)
|
||||
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
organization: haskell-servant
|
||||
repository: servant
|
|
@ -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")]`
|
||||
}
|
38
default.nix
38
default.nix
|
@ -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;
|
||||
}
|
||||
|
4
doc-index.html
Normal file
4
doc-index.html
Normal file
File diff suppressed because one or more lines are too long
22
doc/Makefile
22
doc/Makefile
|
@ -1,22 +0,0 @@
|
|||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
SPHINXPROJ = Servant
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make 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)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# 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)
|
|
@ -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 .
|
171
doc/conf.py
171
doc/conf.py
|
@ -1,171 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Servant documentation build configuration file, created by
|
||||
# sphinx-quickstart on Fri Jul 6 11:38:51 2018.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#
|
||||
# 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
|
||||
# ones.
|
||||
extensions = []
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
source_suffix = ['.rst', '.md', '.lhs']
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'Servant'
|
||||
copyright = u'2022, Servant Contributors'
|
||||
author = u'Servant Contributors'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
# version = u'0.14'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
# release = u'0.14'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
|
||||
# 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']
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = False
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# 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 = {}
|
||||
|
||||
# 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',
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'Servantdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# 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',
|
||||
u'Servant Contributors', 'manual'),
|
||||
]
|
||||
|
||||
|
||||
# -- 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',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (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'),
|
||||
]
|
||||
|
||||
# -- Markdown -------------------------------------------------------------
|
||||
|
||||
source_parsers = {
|
||||
'.lhs': CommonMarkParser,
|
||||
}
|
|
@ -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.
|
|
@ -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
|
|
@ -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"
|
||||
```
|
|
@ -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
|
|
@ -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
|
|
@ -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.
|
|
@ -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
|
|
@ -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.
|
|
@ -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
|
|
@ -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'
|
||||
```
|
|
@ -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
|
|
@ -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).
|
|
@ -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
|
|
@ -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).
|
|
@ -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
|
|
@ -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).
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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).
|
|
@ -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
|
|
@ -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).
|
|
@ -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
|
|
@ -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
|
|
@ -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).
|
|
@ -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
|
|
@ -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.
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
```
|
|
@ -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 }
|
||||
```
|
|
@ -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
|
|
@ -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) we’ll 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.
|
|
@ -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
|
|
@ -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).
|
|
@ -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
|
|
@ -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).
|
|
@ -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
|
|
@ -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.
|
|
@ -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
|
|
@ -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
|
||||
```
|
|
@ -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
|
|
@ -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 ()
|
||||
```
|
|
@ -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
|
|
@ -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.
|
|
@ -1,26 +0,0 @@
|
|||
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.
|
||||
|
||||
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.
|
||||
|
||||
If you would like to learn more, click the tutorial link below.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
tutorial/index.rst
|
||||
cookbook/index.rst
|
||||
examples.md
|
||||
links.rst
|
||||
principles.rst
|
|
@ -1,34 +0,0 @@
|
|||
|
||||
Helpful Links
|
||||
-------------
|
||||
|
||||
- the central documentation (this site):
|
||||
`docs.servant.dev <http://docs.servant.dev/>`_
|
||||
|
||||
- the github repo:
|
||||
`github.com/haskell-servant/servant <https://github.com/haskell-servant/servant>`_
|
||||
|
||||
- the issue tracker (Feel free to create issues and submit PRs!):
|
||||
`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>`_
|
||||
|
||||
- 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>`_
|
||||
|
||||
- the servant packages on hackage:
|
||||
|
||||
- `hackage.haskell.org/package/servant <http://hackage.haskell.org/package/servant>`_
|
||||
- `hackage.haskell.org/package/servant-server <http://hackage.haskell.org/package/servant-server>`_
|
||||
- `hackage.haskell.org/package/servant-client <http://hackage.haskell.org/package/servant-client>`_
|
||||
- `hackage.haskell.org/package/servant-blaze <http://hackage.haskell.org/package/servant-blaze>`_
|
||||
- `hackage.haskell.org/package/servant-lucid <http://hackage.haskell.org/package/servant-lucid>`_
|
||||
- `hackage.haskell.org/package/servant-cassava <http://hackage.haskell.org/package/servant-cassava>`_
|
||||
- `hackage.haskell.org/package/servant-docs <http://hackage.haskell.org/package/servant-docs>`_
|
||||
- `hackage.haskell.org/package/servant-foreign <http://hackage.haskell.org/package/servant-foreign>`_
|
||||
- `hackage.haskell.org/package/servant-js <http://hackage.haskell.org/package/servant-js>`_
|
||||
- `hackage.haskell.org/package/servant-mock <http://hackage.haskell.org/package/servant-mock>`_
|
|
@ -1,40 +0,0 @@
|
|||
Principles
|
||||
----------
|
||||
|
||||
**servant** has the following guiding principles:
|
||||
|
||||
- concision
|
||||
|
||||
This is a pretty wide-ranging principle. You should be able to get nice
|
||||
documentation for your web servers, and client libraries, without repeating
|
||||
yourself. You should not have to manually serialize and deserialize your
|
||||
resources, but only declare how to do those things *once per type*. If a
|
||||
bunch of your handlers take the same query parameters, you shouldn't have to
|
||||
repeat that logic for each handler, but instead just "apply" it to all of
|
||||
them at once. Your handlers shouldn't be where composition goes to die. And
|
||||
so on.
|
||||
|
||||
- flexibility
|
||||
|
||||
If we haven't thought of your use case, it should still be easily
|
||||
achievable. If you want to use templating library X, go ahead. Forms? Do
|
||||
them however you want, but without difficulty. We're not opinionated.
|
||||
|
||||
- separation of concerns
|
||||
|
||||
Your handlers and your HTTP logic should be separate. True to the philosophy
|
||||
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
|
||||
that's just one example.
|
||||
|
||||
- type safety
|
||||
|
||||
Want to be sure your API meets a specification? Your compiler can check
|
||||
that for you. Links you can be sure exist? You got it.
|
||||
|
||||
To stick true to these principles, we do things a little differently than you
|
||||
might expect. The core idea is *reifying the description of your API*. Once
|
||||
reified, everything follows. We think we might be the first web framework to
|
||||
reify API descriptions in an extensible way. We're pretty sure we're the first
|
||||
to reify it as *types*.
|
|
@ -1,4 +0,0 @@
|
|||
recommonmark==0.5.0
|
||||
Sphinx==1.8.4
|
||||
sphinx_rtd_theme>=0.4.2
|
||||
jinja2<3.1.0
|
|
@ -1 +0,0 @@
|
|||
:set -pgmL markdown-unlit -Wall -Werror -fno-warn-missing-methods -fno-warn-name-shadowing -itest
|
|
@ -1,418 +0,0 @@
|
|||
# A web API as a type
|
||||
|
||||
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 #-}
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
|
||||
module ApiType where
|
||||
|
||||
import Data.Text
|
||||
import Data.Time (UTCTime)
|
||||
import Servant.API
|
||||
```
|
||||
|
||||
Consider the following informal specification of an API:
|
||||
|
||||
> The endpoint at `/users` expects a GET request with query string parameter
|
||||
> `sortby` whose value can be one of `age` or `name` and returns a
|
||||
> list/array of JSON objects describing users, with fields `age`, `name`,
|
||||
> `email`, `registration_date`".
|
||||
|
||||
You *should* be able to formalize that. And then use the formalized version to
|
||||
get you much of the way towards writing a web app. And all the way towards
|
||||
getting some client libraries, and documentation, and more.
|
||||
|
||||
How would we describe it with **servant**? An endpoint description is a good old
|
||||
Haskell **type**:
|
||||
|
||||
``` haskell
|
||||
type UserAPI = "users" :> QueryParam "sortby" SortBy :> Get '[JSON] [User]
|
||||
|
||||
data SortBy = Age | Name
|
||||
|
||||
data User = User {
|
||||
name :: String,
|
||||
age :: Int,
|
||||
email :: String,
|
||||
registration_date :: UTCTime
|
||||
}
|
||||
```
|
||||
|
||||
Let's break that down:
|
||||
|
||||
- `"users"` says that our endpoint will be accessible under `/users`;
|
||||
- `QueryParam "sortby" SortBy`, where `SortBy` is defined by `data SortBy = Age | Name`,
|
||||
says that the endpoint has a query string parameter named `sortby`
|
||||
whose value will be extracted as a value of type `SortBy`.
|
||||
- `Get '[JSON] [User]` says that the endpoint will be accessible through HTTP
|
||||
GET requests, returning a list of users encoded as JSON. You will see
|
||||
later how you can make use of this to make your data available under different
|
||||
formats, the choice being made depending on the [Accept
|
||||
header](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html) specified in
|
||||
the client's request.
|
||||
- The `:>` operator that separates the various "combinators" just lets you
|
||||
sequence static path fragments, URL captures and other combinators. The
|
||||
ordering only matters for static path fragments and URL captures. `"users" :>
|
||||
"list-all" :> Get '[JSON] [User]`, equivalent to `/users/list-all`, is
|
||||
obviously not the same as `"list-all" :> "users" :> Get '[JSON] [User]`, which
|
||||
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:
|
||||
|
||||
``` haskell
|
||||
type UserAPI2 = "users" :> "list-all" :> Get '[JSON] [User]
|
||||
:<|> "list-all" :> "users" :> 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.
|
||||
|
||||
## Combinators
|
||||
|
||||
### Static strings
|
||||
|
||||
As you've already seen, you can use type-level strings (enabled with the
|
||||
`DataKinds` language extension) for static path fragments. Chaining
|
||||
them amounts to `/`-separating them in a URL.
|
||||
|
||||
``` haskell
|
||||
type UserAPI3 = "users" :> "list-all" :> "now" :> Get '[JSON] [User]
|
||||
-- describes an endpoint reachable at:
|
||||
-- /users/list-all/now
|
||||
```
|
||||
|
||||
### `Delete`, `Get`, `Patch`, `Post` and `Put`
|
||||
|
||||
The `Get` combinator is defined in terms of the more general `Verb`:
|
||||
``` haskell ignore
|
||||
data Verb method (statusCode :: Nat) (contentType :: [*]) a
|
||||
type Get = Verb 'GET 200
|
||||
```
|
||||
|
||||
There are other predefined type synonyms for other common HTTP methods,
|
||||
such as e.g.:
|
||||
``` haskell ignore
|
||||
type Delete = Verb 'DELETE 200
|
||||
type Patch = Verb 'PATCH 200
|
||||
type Post = Verb 'POST 200
|
||||
type Put = Verb 'PUT 200
|
||||
```
|
||||
|
||||
There are also variants that do not return a 200 status code, such
|
||||
as for example:
|
||||
``` haskell ignore
|
||||
type PostCreated = Verb 'POST 201
|
||||
type PostAccepted = Verb 'POST 202
|
||||
```
|
||||
|
||||
An endpoint always ends with a variant of the `Verb` combinator
|
||||
(unless you write your own combinators). Examples:
|
||||
|
||||
``` haskell
|
||||
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
|
||||
captured and passed to the request handlers. In many web frameworks, you'll see
|
||||
it written as in `/users/:userid`, with that leading `:` denoting that `userid`
|
||||
is just some kind of variable name or placeholder. For instance, if `userid` is
|
||||
supposed to range over all integers greater or equal to 1, our endpoint will
|
||||
match requests made to `/users/1`, `/users/143` and so on.
|
||||
|
||||
The `Capture` combinator in **servant** takes a (type-level) string representing
|
||||
the "name of the variable" and a type, which indicates the type we want to
|
||||
decode the "captured value" to.
|
||||
|
||||
``` haskell ignore
|
||||
data Capture (s :: Symbol) a
|
||||
-- s :: Symbol just says that 's' must be a type-level string.
|
||||
```
|
||||
|
||||
In some web frameworks, you use regexes for captures. We use a
|
||||
[`FromHttpApiData`](https://hackage.haskell.org/package/http-api-data/docs/Web-HttpApiData.html#t:FromHttpApiData)
|
||||
class, which the captured value must be an instance of.
|
||||
|
||||
Examples:
|
||||
|
||||
``` haskell
|
||||
type UserAPI5 = "user" :> Capture "userid" Integer :> Get '[JSON] User
|
||||
-- equivalent to 'GET /user/:userid'
|
||||
-- except that we explicitly say that "userid"
|
||||
-- must be an integer
|
||||
|
||||
:<|> "user" :> Capture "userid" Integer :> DeleteNoContent
|
||||
-- equivalent to 'DELETE /user/:userid'
|
||||
```
|
||||
|
||||
In the second case, `DeleteNoContent` specifies a 204 response code
|
||||
and that the response will always be empty.
|
||||
|
||||
### `QueryParam`, `QueryParams`, `QueryFlag`
|
||||
|
||||
`QueryParam`, `QueryParams` and `QueryFlag` are about parameters in the query string,
|
||||
i.e., those parameters that come after the question mark
|
||||
(`?`) in URLs, like `sortby` in `/users?sortby=age`, whose value is
|
||||
set to `age`. `QueryParams` lets you specify that the query parameter
|
||||
is actually a list of values, which can be specified using
|
||||
`?param=value1¶m=value2`. This represents a list of values
|
||||
composed of `value1` and `value2`. `QueryFlag` lets you specify a
|
||||
boolean-like query parameter where a client isn't forced to specify a
|
||||
value. The absence or presence of the parameter's name in the query
|
||||
string determines whether the parameter is considered to have the
|
||||
value `True` or `False`. For instance, `/users?active` would list only
|
||||
active users whereas `/users` would list them all.
|
||||
|
||||
Here are the corresponding data type declarations:
|
||||
|
||||
``` haskell ignore
|
||||
data QueryParam (sym :: Symbol) a
|
||||
data QueryParams (sym :: Symbol) a
|
||||
data QueryFlag (sym :: Symbol)
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
``` haskell
|
||||
type UserAPI6 = "users" :> QueryParam "sortby" SortBy :> Get '[JSON] [User]
|
||||
-- equivalent to 'GET /users?sortby={age, name}'
|
||||
|
||||
```
|
||||
|
||||
Again, your handlers don't have to deserialize these things (into, for example,
|
||||
a `SortBy`). **servant** takes care of it.
|
||||
|
||||
### `ReqBody`
|
||||
|
||||
Each HTTP request can carry some additional data that the server can use in its
|
||||
*body*, and this data can be encoded in any format -- as long as the server
|
||||
understands it. This can be used for example for an endpoint for creating new
|
||||
users: instead of passing each field of the user as a separate query string
|
||||
parameter or something dirty like that, we can group all the data into a JSON
|
||||
object. This has the advantage of supporting nested objects.
|
||||
|
||||
**servant**'s `ReqBody` combinator takes a list of content types in which the
|
||||
data encoded in the request body can be represented and the type of that data.
|
||||
And, as you might have guessed, you don't have to check the content type
|
||||
header, and do the deserialization yourself. We do it for you. And return `Bad
|
||||
Request` or `Unsupported Content Type` as appropriate.
|
||||
|
||||
Here's the data type declaration for it:
|
||||
|
||||
``` haskell ignore
|
||||
data ReqBody (contentTypes :: [*]) a
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
``` haskell
|
||||
type UserAPI7 = "users" :> ReqBody '[JSON] User :> Post '[JSON] User
|
||||
-- - equivalent to 'POST /users' with a JSON object
|
||||
-- describing a User in the request body
|
||||
-- - returns a User encoded in JSON
|
||||
|
||||
:<|> "users" :> Capture "userid" Integer
|
||||
:> ReqBody '[JSON] User
|
||||
:> Put '[JSON] User
|
||||
-- - equivalent to 'PUT /users/:userid' with a JSON
|
||||
-- object describing a User in the request body
|
||||
-- - returns a User encoded in JSON
|
||||
```
|
||||
|
||||
### Request `Header`s
|
||||
|
||||
Request headers are used for various purposes, from caching to carrying
|
||||
auth-related data. They consist of a header name and an associated value. An
|
||||
example would be `Accept: application/json`.
|
||||
|
||||
The `Header` combinator in **servant** takes a type-level string for the header
|
||||
name and the type to which we want to decode the header's value (from some
|
||||
textual representation), as illustrated below:
|
||||
|
||||
``` haskell ignore
|
||||
data Header (sym :: Symbol) a
|
||||
```
|
||||
|
||||
Here's an example where we declare that an endpoint makes use of the
|
||||
`User-Agent` header which specifies the name of the software/library used by
|
||||
the client to send the request.
|
||||
|
||||
``` haskell
|
||||
type UserAPI8 = "users" :> Header "User-Agent" Text :> Get '[JSON] [User]
|
||||
```
|
||||
|
||||
### Content types
|
||||
|
||||
So far, whenever we have used a combinator that carries a list of content
|
||||
types, we've always specified `'[JSON]`. However, **servant** lets you use several
|
||||
content types, and also lets you define your own content types.
|
||||
|
||||
Four content types are provided out-of-the-box by the core **servant** package:
|
||||
`JSON`, `PlainText`, `FormUrlEncoded` and `OctetStream`. If for some obscure
|
||||
reason you wanted one of your endpoints to make your user data available under
|
||||
those 4 formats, you would write the API type as below:
|
||||
|
||||
``` haskell
|
||||
type UserAPI9 = "users" :> Get '[JSON, PlainText, FormUrlEncoded, OctetStream] [User]
|
||||
```
|
||||
|
||||
(There are other packages that provide other content types. For example
|
||||
**servant-lucid** and **servant-blaze** allow to generate html pages (using
|
||||
**lucid** and **blaze-html**) and both come with a content type for html.)
|
||||
|
||||
We will further explain how these content types and your data types can play
|
||||
together in the [section about serving an API](Server.html).
|
||||
|
||||
### Response `Headers`
|
||||
|
||||
Just like an HTTP request, the response generated by a webserver can carry
|
||||
headers too. **servant** provides a `Headers` combinator that carries a list of
|
||||
`Header` types and can be used by simply wrapping the "return type" of an endpoint
|
||||
with it.
|
||||
|
||||
``` haskell ignore
|
||||
data Headers (ls :: [*]) a
|
||||
```
|
||||
|
||||
If you want to describe an endpoint that returns a "User-Count" header in each
|
||||
response, you could write it as below:
|
||||
|
||||
``` haskell
|
||||
type UserAPI10 = "users" :> Get '[JSON] (Headers '[Header "User-Count" Integer] [User])
|
||||
```
|
||||
|
||||
### Basic Authentication
|
||||
|
||||
Once you've established the basic routes and semantics of your API, it's time
|
||||
to consider protecting parts of it. Authentication and authorization are broad
|
||||
and nuanced topics; as servant began to explore this space we started small
|
||||
with one of HTTP's earliest authentication schemes: [Basic
|
||||
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.
|
||||
2. The datatype returned by the server after authentication is verified. This
|
||||
is usually a `User` or `Customer` type datatype.
|
||||
|
||||
With those two items in mind, *servant* provides the following combinator:
|
||||
|
||||
``` haskell ignore
|
||||
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
|
||||
```
|
||||
|
||||
### 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
|
||||
to the underlying low-level web library `wai`. It can be used when
|
||||
you want to plug a [wai `Application`](http://hackage.haskell.org/package/wai)
|
||||
into your webservice:
|
||||
|
||||
``` haskell
|
||||
type UserAPI13 = "users" :> Get '[JSON] [User]
|
||||
-- a /users endpoint
|
||||
|
||||
:<|> Raw
|
||||
-- requests to anything else than /users
|
||||
-- go here, where the server will try to
|
||||
-- find a file with the right name
|
||||
-- at the right path
|
||||
```
|
||||
|
||||
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.
|
||||
```
|
|
@ -1,439 +0,0 @@
|
|||
# Authentication in Servant
|
||||
|
||||
Once you've established the basic routes and semantics of your API, it's time
|
||||
to consider protecting parts of it. Authentication and authorization are broad
|
||||
and nuanced topics; as servant began to explore this space we started small
|
||||
with one of HTTP's earliest authentication schemes: [Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication).
|
||||
|
||||
Servant `0.5` shipped with out-of-the-box support for Basic Authentication.
|
||||
However, we recognize that every web application is its own beautiful snowflake
|
||||
and are offering experimental support for generalized or ad-hoc authentication.
|
||||
|
||||
In this tutorial we'll build two APIs. One protecting certain routes with Basic
|
||||
Authentication and another protecting the same routes with a custom, in-house
|
||||
authentication scheme.
|
||||
|
||||
## Basic Authentication
|
||||
|
||||
When protecting endpoints with basic authentication, we need to specify two
|
||||
items:
|
||||
|
||||
1. The **realm** of authentication as per the Basic Authentication spec.
|
||||
2. The datatype returned by the server after authentication is verified. This
|
||||
is usually a `User` or `Customer` datatype.
|
||||
|
||||
With those two items in mind, *servant* provides the following combinator:
|
||||
|
||||
``` haskell ignore
|
||||
data BasicAuth (realm :: Symbol) (userData :: *)
|
||||
```
|
||||
|
||||
You can use this combinator to protect an API as follows:
|
||||
|
||||
```haskell
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
|
||||
module Authentication where
|
||||
|
||||
import Data.Aeson (ToJSON)
|
||||
import Data.ByteString (ByteString)
|
||||
import Data.Map (Map, fromList)
|
||||
import qualified Data.Map as Map
|
||||
import Data.Proxy (Proxy (Proxy))
|
||||
import Data.Text (Text)
|
||||
import GHC.Generics (Generic)
|
||||
import Network.Wai (Request, requestHeaders)
|
||||
import Network.Wai.Handler.Warp (run)
|
||||
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)
|
||||
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 }
|
||||
deriving (Eq, Show, Generic)
|
||||
|
||||
instance ToJSON PrivateData
|
||||
|
||||
-- | public data that anyone can use.
|
||||
newtype PublicData = PublicData { somedata :: Text }
|
||||
deriving (Eq, Show, Generic)
|
||||
|
||||
instance ToJSON PublicData
|
||||
|
||||
-- | A user we'll grab from the database when we authenticate someone
|
||||
newtype User = User { userName :: Text }
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- | a type to wrap our public api
|
||||
type PublicAPI = Get '[JSON] [PublicData]
|
||||
|
||||
-- | a type to wrap our private api
|
||||
type PrivateAPI = Get '[JSON] PrivateData
|
||||
|
||||
-- | our API
|
||||
type BasicAPI = "public" :> PublicAPI
|
||||
:<|> "private" :> BasicAuth "foo-realm" User :> PrivateAPI
|
||||
|
||||
-- | a value holding a proxy of our API type
|
||||
basicAuthApi :: Proxy BasicAPI
|
||||
basicAuthApi = Proxy
|
||||
```
|
||||
|
||||
You can see that we've prefixed our public API with "public" and our private
|
||||
API with "private." Additionally, the private parts of our API use the
|
||||
`BasicAuth` combinator to protect them under a Basic Authentication scheme (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
|
||||
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.
|
||||
|
||||
Until now, all of servant's API combinators extracted information from a request
|
||||
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`
|
||||
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?
|
||||
|
||||
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
|
||||
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
|
||||
different function to better capture the semantics of basic authentication:
|
||||
|
||||
``` haskell ignore
|
||||
-- | 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)
|
||||
```
|
||||
|
||||
We now use this datatype to supply servant with a method to authenticate
|
||||
requests. In this simple example the only valid username and password is
|
||||
`"servant"` and `"server"`, respectively, but in a real, production application
|
||||
you might do some database lookup here.
|
||||
|
||||
```haskell
|
||||
-- | 'BasicAuthCheck' holds the handler we'll use to verify a username and password.
|
||||
authCheck :: BasicAuthCheck User
|
||||
authCheck =
|
||||
let check (BasicAuthData username password) =
|
||||
if username == "servant" && password == "server"
|
||||
then return (Authorized (User "servant"))
|
||||
else return Unauthorized
|
||||
in BasicAuthCheck check
|
||||
```
|
||||
|
||||
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
|
||||
-- to the BasicAuth HasServer handlers.
|
||||
basicAuthServerContext :: Context (BasicAuthCheck User ': '[])
|
||||
basicAuthServerContext = authCheck :. EmptyContext
|
||||
```
|
||||
|
||||
We're now ready to write our `server` method that will tie everything together:
|
||||
|
||||
```haskell
|
||||
-- | an implementation of our server. Here is where we pass all the handlers to our endpoints.
|
||||
-- In particular, for the BasicAuth protected handler, we need to supply a function
|
||||
-- that takes 'User' as an argument.
|
||||
basicAuthServer :: Server BasicAPI
|
||||
basicAuthServer =
|
||||
let publicAPIHandler = return [PublicData "foo", PublicData "bar"]
|
||||
privateAPIHandler (user :: User) = return (PrivateData (userName user))
|
||||
in publicAPIHandler :<|> privateAPIHandler
|
||||
```
|
||||
|
||||
Finally, our main method and a sample session working with our server:
|
||||
|
||||
```haskell
|
||||
-- | hello, server!
|
||||
basicAuthMain :: IO ()
|
||||
basicAuthMain = run 8080 (serveWithContext basicAuthApi
|
||||
basicAuthServerContext
|
||||
basicAuthServer
|
||||
)
|
||||
|
||||
{- Sample session
|
||||
|
||||
$ curl -XGET localhost:8080/public
|
||||
[{"somedata":"foo"},{"somedata":"bar"}
|
||||
|
||||
$ curl -iXGET localhost:8080/private
|
||||
HTTP/1.1 401 Unauthorized
|
||||
transfer-encoding: chunked
|
||||
Date: Thu, 07 Jan 2016 22:36:38 GMT
|
||||
Server: Warp/3.1.8
|
||||
WWW-Authenticate: Basic realm="foo-realm"
|
||||
|
||||
$ curl -iXGET localhost:8080/private -H "Authorization: Basic c2VydmFudDpzZXJ2ZXI="
|
||||
HTTP/1.1 200 OK
|
||||
transfer-encoding: chunked
|
||||
Date: Thu, 07 Jan 2016 22:37:58 GMT
|
||||
Server: Warp/3.1.8
|
||||
Content-Type: application/json
|
||||
{"ssshhh":"servant"}
|
||||
-}
|
||||
```
|
||||
|
||||
## Generalized Authentication
|
||||
|
||||
Sometimes your server's authentication scheme doesn't quite fit with the
|
||||
standards (or perhaps servant hasn't rolled-out support for that new, fancy
|
||||
authentication scheme). For such a scenario, servant `0.5` provides easy and
|
||||
simple experimental support to roll your own authentication.
|
||||
|
||||
Why experimental? We worked on the design for authentication for a long time. We
|
||||
really struggled to find a nice, type-safe niche in the design space. In fact,
|
||||
`Context` came out of this work, and while it really fit for schemes like Basic
|
||||
and JWT, it wasn't enough to fully support something like OAuth or HMAC, which
|
||||
have flows, roles, and other fancy ceremonies. Further, we weren't sure *how*
|
||||
people will use auth.
|
||||
|
||||
So, in typical startup fashion, we developed an MVP of 'generalized auth' and
|
||||
released it in an experimental module, with the hope of getting feedback from you!
|
||||
So, if you're reading this or using generalized auth support, please give us
|
||||
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.
|
||||
|
||||
### 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`.
|
||||
|
||||
```haskell
|
||||
-- | An account type that we "fetch from the database" after
|
||||
-- performing authentication
|
||||
newtype Account = Account { unAccount :: Text }
|
||||
|
||||
-- | A (pure) database mapping keys to accounts.
|
||||
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.
|
||||
-- This is our bespoke (and bad) authentication logic.
|
||||
lookupAccount :: ByteString -> Handler Account
|
||||
lookupAccount key = case Map.lookup key database of
|
||||
Nothing -> throwError (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
|
||||
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):
|
||||
|
||||
```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.
|
||||
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
|
||||
```
|
||||
|
||||
Let's now protect our API with our new, bespoke authentication scheme. We'll
|
||||
re-use the endpoints from our Basic Authentication example.
|
||||
|
||||
```haskell
|
||||
-- | Our API, with auth-protection
|
||||
type AuthGenAPI = "private" :> AuthProtect "cookie-auth" :> PrivateAPI
|
||||
:<|> "public" :> PublicAPI
|
||||
|
||||
-- | A value holding our type-level API
|
||||
genAuthAPI :: Proxy AuthGenAPI
|
||||
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)
|
||||
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
|
||||
fails).
|
||||
|
||||
```haskell
|
||||
|
||||
-- | We need to specify the data returned after authentication
|
||||
type instance AuthServerData (AuthProtect "cookie-auth") = Account
|
||||
```
|
||||
|
||||
Note that we specify the type-level tag `"cookie-auth"` when defining the type
|
||||
family instance. This allows us to have multiple authentication schemes
|
||||
protecting a single API.
|
||||
|
||||
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
|
||||
-- "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 ': '[])
|
||||
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,
|
||||
-- that is taken care of by supplying context
|
||||
genAuthServer :: Server AuthGenAPI
|
||||
genAuthServer =
|
||||
let privateDataFunc (Account name) =
|
||||
return (PrivateData ("this is a secret: " <> name))
|
||||
publicData = return [PublicData "this is a public piece of data"]
|
||||
in privateDataFunc :<|> publicData
|
||||
```
|
||||
|
||||
We're now ready to start our server (and provide a sample session)!
|
||||
|
||||
```haskell
|
||||
-- | run our server
|
||||
genAuthMain :: IO ()
|
||||
genAuthMain = run 8080 (serveWithContext genAuthAPI genAuthServerContext genAuthServer)
|
||||
|
||||
{- Sample Session:
|
||||
|
||||
$ curl -XGET localhost:8080/private
|
||||
Missing auth header
|
||||
|
||||
$ curl -XGET localhost:8080/private -H "Cookie: 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"
|
||||
Invalid Cookie
|
||||
|
||||
$ curl -XGET localhost:8080/public
|
||||
[{"somedata":"this is a public piece of data"}]
|
||||
-}
|
||||
```
|
||||
|
||||
### Recap
|
||||
|
||||
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.
|
||||
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`).
|
||||
|
||||
Caveats:
|
||||
|
||||
1. The module `Servant.Server.Experimental.Auth` contains an orphan `HasServer`
|
||||
instance for the `AuthProtect` combinator. You may be get orphan instance
|
||||
warnings when using this.
|
||||
2. Generalized authentication requires the `UndecidableInstances` extension.
|
||||
|
||||
## Client-side Authentication
|
||||
|
||||
### Basic Authentication
|
||||
|
||||
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
|
||||
to the server-side support, clients need to supply an instance of the
|
||||
`AuthClientData` type family specifying the datatype the client will use to
|
||||
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
|
||||
```
|
||||
|
||||
Now, if the client method for our protected endpoint was `getProtected`, then
|
||||
we could perform authenticated requests as follows:
|
||||
|
||||
```haskell ignore
|
||||
-- | one could curry this to make it simpler to work with.
|
||||
result = runExceptT (getProtected (mkAuthenticateReq "secret" authenticateReq))
|
||||
```
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue