Compare commits
1 commit
master
...
release-0.
Author | SHA1 | Date | |
---|---|---|---|
|
87c5e47927 |
302 changed files with 2809 additions and 15547 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"
|
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -1,6 +1,5 @@
|
|||
**/*/dist
|
||||
*~
|
||||
dist-*
|
||||
dist-newstyle
|
||||
.ghc.environment.*
|
||||
/bin
|
||||
/lib
|
||||
|
@ -30,11 +29,6 @@ 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
|
||||
|
|
192
.travis.yml
Normal file
192
.travis.yml
Normal file
|
@ -0,0 +1,192 @@
|
|||
# This Travis job script has been generated by a script via
|
||||
#
|
||||
# runghc make_travis_yml_2.hs '--config=cabal.make-travis-yml' '--output=.travis.yml' 'cabal.project'
|
||||
#
|
||||
# For more information, see https://github.com/haskell-CI/haskell-ci
|
||||
#
|
||||
language: c
|
||||
sudo: false
|
||||
|
||||
git:
|
||||
submodules: false # whether to recursively clone submodules
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cabal/packages
|
||||
- $HOME/.cabal/store
|
||||
|
||||
before_cache:
|
||||
- rm -fv $HOME/.cabal/packages/hackage.haskell.org/build-reports.log
|
||||
# remove files that are regenerated by 'cabal update'
|
||||
- rm -fv $HOME/.cabal/packages/hackage.haskell.org/00-index.*
|
||||
- rm -fv $HOME/.cabal/packages/hackage.haskell.org/*.json
|
||||
- rm -fv $HOME/.cabal/packages/hackage.haskell.org/01-index.cache
|
||||
- rm -fv $HOME/.cabal/packages/hackage.haskell.org/01-index.tar
|
||||
- rm -fv $HOME/.cabal/packages/hackage.haskell.org/01-index.tar.idx
|
||||
|
||||
- rm -rfv $HOME/.cabal/packages/head.hackage
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- compiler: "ghc-8.6.2"
|
||||
# env: TEST=--disable-tests BENCH=--disable-benchmarks
|
||||
addons: {apt: {packages: [ghc-ppa-tools,cabal-install-2.4,ghc-8.6.2], sources: [hvr-ghc]}}
|
||||
- compiler: "ghc-8.4.4"
|
||||
# env: TEST=--disable-tests BENCH=--disable-benchmarks
|
||||
addons: {apt: {packages: [ghc-ppa-tools,cabal-install-2.4,ghc-8.4.4], sources: [hvr-ghc]}}
|
||||
- compiler: "ghc-8.2.2"
|
||||
# env: TEST=--disable-tests BENCH=--disable-benchmarks
|
||||
addons: {apt: {packages: [ghc-ppa-tools,cabal-install-2.4,ghc-8.2.2], sources: [hvr-ghc]}}
|
||||
- compiler: "ghc-8.0.2"
|
||||
# env: TEST=--disable-tests BENCH=--disable-benchmarks
|
||||
addons: {apt: {packages: [ghc-ppa-tools,cabal-install-2.4,ghc-8.0.2], sources: [hvr-ghc]}}
|
||||
|
||||
before_install:
|
||||
- HC=${CC}
|
||||
- HCPKG=${HC/ghc/ghc-pkg}
|
||||
- unset CC
|
||||
- ROOTDIR=$(pwd)
|
||||
- mkdir -p $HOME/.local/bin
|
||||
- "PATH=/opt/ghc/bin:/opt/ghc-ppa-tools/bin:$HOME/local/bin:$PATH"
|
||||
- HCNUMVER=$(( $(${HC} --numeric-version|sed -E 's/([0-9]+)\.([0-9]+)\.([0-9]+).*/\1 * 10000 + \2 * 100 + \3/') ))
|
||||
- echo $HCNUMVER
|
||||
|
||||
install:
|
||||
- cabal --version
|
||||
- echo "$(${HC} --version) [$(${HC} --print-project-git-commit-id 2> /dev/null || echo '?')]"
|
||||
- BENCH=${BENCH---enable-benchmarks}
|
||||
- TEST=${TEST---enable-tests}
|
||||
- HADDOCK=${HADDOCK-true}
|
||||
- UNCONSTRAINED=${UNCONSTRAINED-true}
|
||||
- NOINSTALLEDCONSTRAINTS=${NOINSTALLEDCONSTRAINTS-false}
|
||||
- GHCHEAD=${GHCHEAD-false}
|
||||
- travis_retry cabal update -v
|
||||
- "sed -i.bak 's/^jobs:/-- jobs:/' ${HOME}/.cabal/config"
|
||||
- rm -fv cabal.project cabal.project.local
|
||||
- "if [ $HCNUMVER -ge 70800 ]; then sed -i.bak 's/-- ghc-options:.*/ghc-options: -j2/' ${HOME}/.cabal/config; fi"
|
||||
- grep -Ev -- '^\s*--' ${HOME}/.cabal/config | grep -Ev '^\s*$'
|
||||
- "printf 'packages: \"servant\" \"servant-client\" \"servant-client-core\" \"servant-docs\" \"servant-foreign\" \"servant-server\" \"doc/tutorial\" \"servant-machines\" \"servant-conduit\" \"servant-pipes\" \"doc/cookbook/basic-auth\" \"doc/cookbook/curl-mock\" \"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/sentry\" \"doc/cookbook/testing\" \"doc/cookbook/structuring-apis\" \"doc/cookbook/using-custom-monad\" \"doc/cookbook/using-free-client\"\\n' > cabal.project"
|
||||
- "printf 'write-ghc-environment-files: always\\n' >> cabal.project"
|
||||
- "echo 'constraints: foundation >=0.0.14,memory <0.14.12 || >0.14.12' >> cabal.project"
|
||||
- "echo 'allow-newer: servant-js:base, servant-pagination:servant, servant-pagination:servant-server,servant-multipart:servant, servant-multipart:servant-server,servant-quickcheck:servant, servant-quickcheck:servant-client, servant-quickcheck:servant-server,servant-auth-server:servant, servant-auth-server:servant-server, servant-auth-server:http-api-data,servant-js:servant, servant-js:servant-foreign,servant-quickcheck:hspec,servant-quickcheck:http-client' >> cabal.project"
|
||||
- touch cabal.project.local
|
||||
- "if ! $NOINSTALLEDCONSTRAINTS; then for pkg in $($HCPKG list --simple-output); do echo $pkg | grep -vw -- servant | grep -vw -- servant-client | grep -vw -- servant-client-core | grep -vw -- servant-docs | grep -vw -- servant-foreign | grep -vw -- servant-server | grep -vw -- tutorial | grep -vw -- servant-machines | grep -vw -- servant-conduit | grep -vw -- servant-pipes | grep -vw -- cookbook-basic-auth | grep -vw -- cookbook-curl-mock | grep -vw -- cookbook-basic-streaming | grep -vw -- cookbook-db-postgres-pool | grep -vw -- cookbook-db-sqlite-simple | grep -vw -- cookbook-file-upload | grep -vw -- cookbook-generic | grep -vw -- cookbook-hoist-server-with-context | grep -vw -- cookbook-https | grep -vw -- cookbook-jwt-and-basic-auth | grep -vw -- cookbook-sentry | grep -vw -- cookbook-testing | grep -vw -- cookbook-structuring-apis | grep -vw -- cookbook-using-custom-monad | grep -vw -- cookbook-using-free-client | sed 's/^/constraints: /' | sed 's/-[^-]*$/ installed/' >> cabal.project.local; done; fi"
|
||||
- cat cabal.project || true
|
||||
- cat cabal.project.local || true
|
||||
- if [ -f "servant/configure.ac" ]; then
|
||||
(cd "servant" && autoreconf -i);
|
||||
fi
|
||||
- if [ -f "servant-client/configure.ac" ]; then
|
||||
(cd "servant-client" && autoreconf -i);
|
||||
fi
|
||||
- if [ -f "servant-client-core/configure.ac" ]; then
|
||||
(cd "servant-client-core" && autoreconf -i);
|
||||
fi
|
||||
- if [ -f "servant-docs/configure.ac" ]; then
|
||||
(cd "servant-docs" && autoreconf -i);
|
||||
fi
|
||||
- if [ -f "servant-foreign/configure.ac" ]; then
|
||||
(cd "servant-foreign" && autoreconf -i);
|
||||
fi
|
||||
- if [ -f "servant-server/configure.ac" ]; then
|
||||
(cd "servant-server" && autoreconf -i);
|
||||
fi
|
||||
- if [ -f "doc/tutorial/configure.ac" ]; then
|
||||
(cd "doc/tutorial" && autoreconf -i);
|
||||
fi
|
||||
- if [ -f "servant-machines/configure.ac" ]; then
|
||||
(cd "servant-machines" && autoreconf -i);
|
||||
fi
|
||||
- if [ -f "servant-conduit/configure.ac" ]; then
|
||||
(cd "servant-conduit" && autoreconf -i);
|
||||
fi
|
||||
- if [ -f "servant-pipes/configure.ac" ]; then
|
||||
(cd "servant-pipes" && autoreconf -i);
|
||||
fi
|
||||
- if [ -f "doc/cookbook/basic-auth/configure.ac" ]; then
|
||||
(cd "doc/cookbook/basic-auth" && autoreconf -i);
|
||||
fi
|
||||
- if [ -f "doc/cookbook/curl-mock/configure.ac" ]; then
|
||||
(cd "doc/cookbook/curl-mock" && autoreconf -i);
|
||||
fi
|
||||
- if [ -f "doc/cookbook/basic-streaming/configure.ac" ]; then
|
||||
(cd "doc/cookbook/basic-streaming" && autoreconf -i);
|
||||
fi
|
||||
- if [ -f "doc/cookbook/db-postgres-pool/configure.ac" ]; then
|
||||
(cd "doc/cookbook/db-postgres-pool" && autoreconf -i);
|
||||
fi
|
||||
- if [ -f "doc/cookbook/db-sqlite-simple/configure.ac" ]; then
|
||||
(cd "doc/cookbook/db-sqlite-simple" && autoreconf -i);
|
||||
fi
|
||||
- if [ -f "doc/cookbook/file-upload/configure.ac" ]; then
|
||||
(cd "doc/cookbook/file-upload" && autoreconf -i);
|
||||
fi
|
||||
- if [ -f "doc/cookbook/generic/configure.ac" ]; then
|
||||
(cd "doc/cookbook/generic" && autoreconf -i);
|
||||
fi
|
||||
- if [ -f "doc/cookbook/hoist-server-with-context/configure.ac" ]; then
|
||||
(cd "doc/cookbook/hoist-server-with-context" && autoreconf -i);
|
||||
fi
|
||||
- if [ -f "doc/cookbook/https/configure.ac" ]; then
|
||||
(cd "doc/cookbook/https" && autoreconf -i);
|
||||
fi
|
||||
- if [ -f "doc/cookbook/jwt-and-basic-auth/configure.ac" ]; then
|
||||
(cd "doc/cookbook/jwt-and-basic-auth" && autoreconf -i);
|
||||
fi
|
||||
- if [ -f "doc/cookbook/sentry/configure.ac" ]; then
|
||||
(cd "doc/cookbook/sentry" && autoreconf -i);
|
||||
fi
|
||||
- if [ -f "doc/cookbook/testing/configure.ac" ]; then
|
||||
(cd "doc/cookbook/testing" && autoreconf -i);
|
||||
fi
|
||||
- if [ -f "doc/cookbook/structuring-apis/configure.ac" ]; then
|
||||
(cd "doc/cookbook/structuring-apis" && autoreconf -i);
|
||||
fi
|
||||
- if [ -f "doc/cookbook/using-custom-monad/configure.ac" ]; then
|
||||
(cd "doc/cookbook/using-custom-monad" && autoreconf -i);
|
||||
fi
|
||||
- if [ -f "doc/cookbook/using-free-client/configure.ac" ]; then
|
||||
(cd "doc/cookbook/using-free-client" && autoreconf -i);
|
||||
fi
|
||||
- rm -f cabal.project.freeze
|
||||
- rm -rf .ghc.environment.* "servant"/dist "servant-client"/dist "servant-client-core"/dist "servant-docs"/dist "servant-foreign"/dist "servant-server"/dist "doc/tutorial"/dist "servant-machines"/dist "servant-conduit"/dist "servant-pipes"/dist "doc/cookbook/basic-auth"/dist "doc/cookbook/curl-mock"/dist "doc/cookbook/basic-streaming"/dist "doc/cookbook/db-postgres-pool"/dist "doc/cookbook/db-sqlite-simple"/dist "doc/cookbook/file-upload"/dist "doc/cookbook/generic"/dist "doc/cookbook/hoist-server-with-context"/dist "doc/cookbook/https"/dist "doc/cookbook/jwt-and-basic-auth"/dist "doc/cookbook/sentry"/dist "doc/cookbook/testing"/dist "doc/cookbook/structuring-apis"/dist "doc/cookbook/using-custom-monad"/dist "doc/cookbook/using-free-client"/dist
|
||||
- DISTDIR=$(mktemp -d /tmp/dist-test.XXXX)
|
||||
|
||||
# Here starts the actual work to be performed for the package under test;
|
||||
# any command which exits with a non-zero exit code causes the build to fail.
|
||||
script:
|
||||
# test that source-distributions can be generated
|
||||
- echo Packaging... && echo -en 'travis_fold:start:sdist\\r'
|
||||
- cabal new-sdist all
|
||||
- echo -en 'travis_fold:end:sdist\\r'
|
||||
- echo Unpacking... && echo -en 'travis_fold:start:unpack\\r'
|
||||
- mv dist-newstyle/sdist/*.tar.gz ${DISTDIR}/
|
||||
- cd ${DISTDIR} || false
|
||||
- find . -maxdepth 1 -name '*.tar.gz' -exec tar -xvf '{}' \;
|
||||
- "printf 'packages: servant-*/*.cabal servant-client-*/*.cabal servant-client-core-*/*.cabal servant-docs-*/*.cabal servant-foreign-*/*.cabal servant-server-*/*.cabal tutorial-*/*.cabal servant-machines-*/*.cabal servant-conduit-*/*.cabal servant-pipes-*/*.cabal cookbook-basic-auth-*/*.cabal cookbook-curl-mock-*/*.cabal cookbook-basic-streaming-*/*.cabal cookbook-db-postgres-pool-*/*.cabal cookbook-db-sqlite-simple-*/*.cabal cookbook-file-upload-*/*.cabal cookbook-generic-*/*.cabal cookbook-hoist-server-with-context-*/*.cabal cookbook-https-*/*.cabal cookbook-jwt-and-basic-auth-*/*.cabal cookbook-sentry-*/*.cabal cookbook-testing-*/*.cabal cookbook-structuring-apis-*/*.cabal cookbook-using-custom-monad-*/*.cabal cookbook-using-free-client-*/*.cabal\\n' > cabal.project"
|
||||
- "printf 'write-ghc-environment-files: always\\n' >> cabal.project"
|
||||
- "echo 'constraints: foundation >=0.0.14,memory <0.14.12 || >0.14.12' >> cabal.project"
|
||||
- "echo 'allow-newer: servant-js:base, servant-pagination:servant, servant-pagination:servant-server,servant-multipart:servant, servant-multipart:servant-server,servant-quickcheck:servant, servant-quickcheck:servant-client, servant-quickcheck:servant-server,servant-auth-server:servant, servant-auth-server:servant-server, servant-auth-server:http-api-data,servant-js:servant, servant-js:servant-foreign,servant-quickcheck:hspec,servant-quickcheck:http-client' >> cabal.project"
|
||||
- touch cabal.project.local
|
||||
- "if ! $NOINSTALLEDCONSTRAINTS; then for pkg in $($HCPKG list --simple-output); do echo $pkg | grep -vw -- servant | grep -vw -- servant-client | grep -vw -- servant-client-core | grep -vw -- servant-docs | grep -vw -- servant-foreign | grep -vw -- servant-server | grep -vw -- tutorial | grep -vw -- servant-machines | grep -vw -- servant-conduit | grep -vw -- servant-pipes | grep -vw -- cookbook-basic-auth | grep -vw -- cookbook-curl-mock | grep -vw -- cookbook-basic-streaming | grep -vw -- cookbook-db-postgres-pool | grep -vw -- cookbook-db-sqlite-simple | grep -vw -- cookbook-file-upload | grep -vw -- cookbook-generic | grep -vw -- cookbook-hoist-server-with-context | grep -vw -- cookbook-https | grep -vw -- cookbook-jwt-and-basic-auth | grep -vw -- cookbook-sentry | grep -vw -- cookbook-testing | grep -vw -- cookbook-structuring-apis | grep -vw -- cookbook-using-custom-monad | grep -vw -- cookbook-using-free-client | sed 's/^/constraints: /' | sed 's/-[^-]*$/ installed/' >> cabal.project.local; done; fi"
|
||||
- cat cabal.project || true
|
||||
- cat cabal.project.local || true
|
||||
- echo -en 'travis_fold:end:unpack\\r'
|
||||
|
||||
- echo Building with tests and benchmarks... && echo -en 'travis_fold:start:build-everything\\r'
|
||||
# build & run tests, build benchmarks
|
||||
- cabal new-build -w ${HC} ${TEST} ${BENCH} all
|
||||
- echo -en 'travis_fold:end:build-everything\\r'
|
||||
- if [ "x$TEST" = "x--enable-tests" ]; then cabal new-test -w ${HC} ${TEST} ${BENCH} all; fi
|
||||
|
||||
- echo Haddock... && echo -en 'travis_fold:start:haddock\\r'
|
||||
# haddock
|
||||
- if $HADDOCK; then cabal new-haddock -w ${HC} ${TEST} ${BENCH} all; else echo "Skipping haddock generation";fi
|
||||
|
||||
- echo -en 'travis_fold:end:haddock\\r'
|
||||
# REGENDATA ["--config=cabal.make-travis-yml","--output=.travis.yml","cabal.project"]
|
||||
# EOF
|
|
@ -21,7 +21,6 @@ Or `nix`:
|
|||
./scripts/generate-nix-files.sh # Get up-to-date shell.nix files
|
||||
```
|
||||
|
||||
To build the docs, see `doc/README.md`.
|
||||
|
||||
## General
|
||||
|
||||
|
@ -35,34 +34,7 @@ Some things we like:
|
|||
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.
|
||||
|
||||
}
|
||||
```
|
||||
**Important**: please do not modify the changelog files nor the versions of the servant packages you are sending patches for. We take care of this before every release and do it uniformly for all the servant packages, so there's no need to worry about this for your pull requests.
|
||||
|
||||
## PR process
|
||||
|
||||
|
@ -79,10 +51,8 @@ not been a timely response to a PR, you can ping the Maintainers group (with
|
|||
We encourage people to experiment with new combinators and instances - it is
|
||||
one of the most powerful ways of using `servant`, and a wonderful way of
|
||||
getting to know it better. If you do write a new combinator, we would love to
|
||||
know about it! Either hop on
|
||||
[#haskell-servant on libera.chat](https://web.libera.chat/#haskell-servant) and
|
||||
let us know, or open an issue with the `news` tag (which we will close when we
|
||||
read it).
|
||||
know about it! Either hop on #servant on freenode and let us know, or open an
|
||||
issue with the `news` tag (which we will close when we read it).
|
||||
|
||||
As for adding them to the main repo: maintaining combinators can be expensive,
|
||||
since official combinators must have instances for all classes (and new classes
|
||||
|
@ -105,7 +75,7 @@ the `news` label if you make a new package so we can know about it!
|
|||
|
||||
## Release policy
|
||||
|
||||
We are currently moving to a more aggressive release policy, so that you can get
|
||||
We are currently moving to a more aggresive release policy, so that you can get
|
||||
what you contribute from Hackage fairly soon. However, note that prior to major
|
||||
releases it may take some time in between releases.
|
||||
|
||||
|
|
44
README.md
44
README.md
|
@ -4,16 +4,16 @@
|
|||
|
||||
## Getting Started
|
||||
|
||||
We have a [tutorial](http://docs.servant.dev/en/stable/tutorial/index.html) that
|
||||
We have a [tutorial](http://haskell-servant.readthedocs.org/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/).
|
||||
The central documentation can be found [here](http://haskell-servant.readthedocs.org/).
|
||||
Other blog posts, videos and slides can be found on the
|
||||
[website](http://www.servant.dev/).
|
||||
[website](http://haskell-servant.github.io/).
|
||||
|
||||
If you need help, drop by the IRC channel (#haskell-servant on libera.chat) or [mailing
|
||||
If you need help, drop by the IRC channel (#servant on freenode) or [mailing
|
||||
list](https://groups.google.com/forum/#!forum/haskell-servant).
|
||||
|
||||
## Contributing
|
||||
|
@ -24,7 +24,7 @@ See `CONTRIBUTING.md`
|
|||
|
||||
- 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`
|
||||
- Create a release branch, e.g. `release-0.13`, and *protect it* from accidental force pushes.
|
||||
- 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.
|
||||
|
@ -32,7 +32,7 @@ See `CONTRIBUTING.md`
|
|||
- 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.
|
||||
- Fix them and make PRs: it's 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:
|
||||
|
@ -40,32 +40,22 @@ See `CONTRIBUTING.md`
|
|||
- `git push --tags`
|
||||
- `cabal sdist` and `cabal upload`
|
||||
|
||||
## TechEmpower framework benchmarks
|
||||
## travis
|
||||
|
||||
We develop and maintain the servant TFB entry in https://github.com/haskell-servant/FrameworkBenchmarks/
|
||||
`.travis.yml` is generated using `make-travis-yml` tool, in
|
||||
[multi-ghc-travis](https://github.com/haskell-hvr/multi-ghc-travis) repository.
|
||||
|
||||
To verify (i.e. compile and test that it works)
|
||||
To regenerate the script use (*note:* atm you need to comment `doc/cookbook/` packages).
|
||||
|
||||
```sh
|
||||
./tfb --mode verify --test servant servant-beam servant-psql-simple --type json plaintext db fortune
|
||||
```
|
||||
runghc ~/Documents/other-haskell/multi-ghc-travis/make_travis_yml_2.hs regenerate
|
||||
```
|
||||
|
||||
To compare with `warp`
|
||||
In case Travis jobs fail due failing build of dependency, you can temporarily
|
||||
add `constraints` to the `cabal.project`, and regenerate the `.travis.yml`.
|
||||
For example, the following will disallow single `troublemaker-13.37` package version:
|
||||
|
||||
```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
|
||||
constraints:
|
||||
troublemaker <13.37 && > 13.37
|
||||
```
|
||||
|
||||
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)
|
||||
|
|
|
@ -1,14 +1,24 @@
|
|||
-- Using https://launchpad.net/~hvr/+archive/ubuntu/ghcjs
|
||||
--
|
||||
-- $ cabal new-build --project-file cabal.ghcjs.project all -w /opt/ghcjs/8.4/bin/ghcjs
|
||||
|
||||
packages:
|
||||
servant/
|
||||
servant-client/
|
||||
servant-client-core/
|
||||
servant-client-ghcjs/
|
||||
|
||||
-- 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
|
||||
-- https://github.com/ghcjs/ghcjs/issues/665
|
||||
constraints: primitive <0.6.4
|
||||
|
||||
-- ghcjs-base wants old aeson
|
||||
allow-newer: ghcjs-base:aeson
|
||||
|
||||
-- https://github.com/nomeata/hackage-ghcjs-overlay
|
||||
repository ghcjs-overlay
|
||||
url: http://hackage-ghcjs-overlay.nomeata.de/
|
||||
secure: True
|
||||
root-keys:
|
||||
key-threshold: 0
|
||||
|
|
15
cabal.make-travis-yml
Normal file
15
cabal.make-travis-yml
Normal file
|
@ -0,0 +1,15 @@
|
|||
folds: all-but-test
|
||||
branches: master
|
||||
|
||||
-- We have inplace packages (servant-js) so we skip installing dependencies in a separate step
|
||||
install-dependencies-step: False
|
||||
|
||||
-- this speed-ups the build a little, but we have to check these for release
|
||||
no-tests-no-benchmarks: False
|
||||
unconstrained-step: False
|
||||
|
||||
-- Don't run cabal check, as cookbook examples won't pass it
|
||||
cabal-check: False
|
||||
|
||||
-- ghc-options: -j2
|
||||
jobs: :2
|
|
@ -1,35 +1,19 @@
|
|||
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
|
||||
|
||||
packages: servant/
|
||||
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/
|
||||
-- doc/cookbook/*/*.cabal
|
||||
|
||||
-- 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
|
||||
|
@ -37,18 +21,28 @@ packages:
|
|||
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/jwt-and-basic-auth/
|
||||
-- doc/cookbook/pagination
|
||||
doc/cookbook/sentry
|
||||
doc/cookbook/testing
|
||||
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
|
||||
allow-newer:
|
||||
servant-js:base
|
||||
|
||||
constraints:
|
||||
-- see https://github.com/haskell-infra/hackage-trustees/issues/119
|
||||
foundation >=0.0.14,
|
||||
memory <0.14.12 || >0.14.12
|
||||
|
||||
allow-newer:
|
||||
servant-pagination:servant, servant-pagination:servant-server,
|
||||
servant-multipart:servant, servant-multipart:servant-server,
|
||||
servant-quickcheck:servant, servant-quickcheck:servant-client, servant-quickcheck:servant-server,
|
||||
servant-auth-server:servant, servant-auth-server:servant-server, servant-auth-server:http-api-data,
|
||||
servant-js:servant, servant-js:servant-foreign,
|
||||
servant-quickcheck:hspec,
|
||||
|
||||
servant-quickcheck:http-client
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -10,8 +10,6 @@ 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
|
||||
|
@ -19,4 +17,4 @@ help:
|
|||
# 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)
|
||||
@$(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 .
|
8
doc/building-the-docs
Normal file
8
doc/building-the-docs
Normal file
|
@ -0,0 +1,8 @@
|
|||
To build the docs locally:
|
||||
|
||||
$ virtualenv venv
|
||||
$ . ./venv/bin/activate
|
||||
$ pip install -r requirements.txt
|
||||
$ make html
|
||||
|
||||
Docs will be built in _build/html/index.html .
|
|
@ -46,7 +46,7 @@ master_doc = 'index'
|
|||
|
||||
# General information about the project.
|
||||
project = u'Servant'
|
||||
copyright = u'2022, Servant Contributors'
|
||||
copyright = u'2018, Servant Contributors'
|
||||
author = u'Servant Contributors'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
|
@ -167,5 +167,7 @@ texinfo_documents = [
|
|||
# -- Markdown -------------------------------------------------------------
|
||||
|
||||
source_parsers = {
|
||||
'.md': CommonMarkParser,
|
||||
'.lhs': CommonMarkParser,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
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
|
||||
homepage: http://haskell-servant.readthedocs.org/
|
||||
license: BSD3
|
||||
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
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.0.2, GHC==8.2.2, GHC==8.4.4, GHC==8.6.2
|
||||
|
||||
executable cookbook-basic-auth
|
||||
main-is: BasicAuth.lhs
|
||||
|
|
|
@ -8,10 +8,7 @@ In other words, without streaming libraries.
|
|||
- 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))
|
||||
- This is similar example file, which is bundled with each of the packages (TODO: links)
|
||||
- `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).
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
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
|
||||
homepage: http://haskell-servant.readthedocs.org/
|
||||
license: BSD3
|
||||
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
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.0.2, GHC==8.2.2, GHC==8.4.4, GHC==8.6.2
|
||||
|
||||
executable cookbook-basic-streaming
|
||||
main-is: Streaming.lhs
|
||||
|
@ -17,7 +17,7 @@ executable cookbook-basic-streaming
|
|||
ghc-options: -Wall -pgmL markdown-unlit -threaded -rtsopts
|
||||
|
||||
hs-source-dirs: .
|
||||
build-depends: base >= 4.8 && <5
|
||||
build-depends: base >= 4.8 && <4.13
|
||||
, aeson
|
||||
, bytestring
|
||||
, servant
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
packages:
|
||||
basic-auth/
|
||||
curl-mock/
|
||||
db-mysql-basics/
|
||||
db-sqlite-simple/
|
||||
db-postgres-pool/
|
||||
using-custom-monad/
|
||||
|
@ -13,7 +12,6 @@ packages:
|
|||
pagination/
|
||||
sentry/
|
||||
testing/
|
||||
open-id-connect/
|
||||
../../servant
|
||||
../../servant-server
|
||||
../../servant-client-core
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# 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.
|
||||
This may be usefull 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.
|
||||
|
@ -24,6 +24,7 @@ Language extensions and imports:
|
|||
import Control.Lens ((^.))
|
||||
import Data.Aeson
|
||||
import Data.Aeson.Text
|
||||
import Data.Monoid ((<>))
|
||||
import Data.Proxy (Proxy (Proxy))
|
||||
import Data.Text (Text)
|
||||
import Data.Text.Encoding (decodeUtf8)
|
||||
|
@ -75,7 +76,7 @@ api = Proxy
|
|||
```
|
||||
|
||||
|
||||
## servant-foreign and the HasForeignType Class
|
||||
## servant-forgein 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:
|
||||
|
@ -85,7 +86,7 @@ listFromAPI :: (HasForeign lang ftype api, GenerateList ftype (Foreign ftype api
|
|||
```
|
||||
|
||||
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.
|
||||
[Here](https://hackage.haskell.org/package/servant-foreign-0.11.1/docs/Servant-Foreign.html#t:HasForeignType) 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`.
|
||||
|
@ -129,12 +130,24 @@ generateCurl :: (GenerateList Mocked (Foreign Mocked api), HasForeign NoLang Moc
|
|||
generateCurl p host =
|
||||
fmap T.unlines body
|
||||
where
|
||||
body = mapM (generateEndpoint host)
|
||||
body = foldr (\endp curlCalls -> mCons (generateEndpoint host endp) curlCalls) (return [])
|
||||
$ 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.
|
||||
To understand this function, better start at the end:
|
||||
|
||||
`listFromAPI` gives us a list of endpoints. We iterate over them (`foldr`) and call `generateEndpoint` for every endpoint.
|
||||
|
||||
As generate endpoint will not return `Text` but `IO Text` (remember we need some random bits to mock), we cannot just use the cons operator but need to build `IO [Text]` from `IO Text`s.
|
||||
|
||||
``` haskell
|
||||
mCons :: IO a -> IO [a] -> IO [a]
|
||||
mCons ele list =
|
||||
ele >>= \e -> list >>= \l -> return ( e : l )
|
||||
```
|
||||
|
||||
|
||||
Now comes the juicy part; accessing the endpoints data:
|
||||
|
||||
``` haskell
|
||||
generateEndpoint :: Text -> Req Mocked -> IO Text
|
||||
|
@ -156,7 +169,7 @@ generateEndpoint host req =
|
|||
`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).
|
||||
Just take a look at [the docs](https://hackage.haskell.org/package/servant-foreign-0.11.1/docs/Servant-Foreign.html).
|
||||
|
||||
But how do we get our mocked json string? This seems to be a bit to short to be true:
|
||||
|
||||
|
@ -188,7 +201,7 @@ And now, lets hook it all up in our main function:
|
|||
``` haskell
|
||||
main :: IO ()
|
||||
main =
|
||||
generateCurl api "localhost:8081" >>= T.IO.putStrLn
|
||||
generateCurl api "localhost:8081" >>= (\v -> T.IO.putStrLn v)
|
||||
```
|
||||
|
||||
Done:
|
||||
|
@ -200,6 +213,6 @@ curl -X POST -d '{"email":"wV_b:z!(3DM V","age":10,"name":"=|W"}
|
|||
```
|
||||
|
||||
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.
|
||||
But it correctly generate mock calls for simple POST requests.
|
||||
|
||||
Also, we now know how to use `HasForeignType` and `listFromAPI` to generate anything we want.
|
||||
|
|
|
@ -1,19 +1,16 @@
|
|||
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
|
||||
homepage: http://haskell-servant.readthedocs.org
|
||||
license: BSD3
|
||||
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
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.0.2, GHC==8.2.2, GHC==8.4.4, GHC==8.6.2
|
||||
|
||||
executable cookbock-curl-mock
|
||||
if impl(ghc >= 9.2)
|
||||
-- generic-arbitrary is incompatible
|
||||
buildable: False
|
||||
main-is: CurlMock.lhs
|
||||
build-depends: base == 4.*
|
||||
, aeson
|
||||
|
|
|
@ -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,14 +1,14 @@
|
|||
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
|
||||
homepage: http://haskell-servant.readthedocs.org/
|
||||
license: BSD3
|
||||
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
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.0.2, GHC==8.2.2, GHC==8.4.4, GHC==8.6.2
|
||||
|
||||
executable cookbook-db-postgres-pool
|
||||
main-is: PostgresPool.lhs
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
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
|
||||
homepage: http://haskell-servant.readthedocs.org/
|
||||
license: BSD3
|
||||
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
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.0.2, GHC==8.2.2, GHC==8.4.4, GHC==8.6.2
|
||||
|
||||
executable cookbook-db-sqlite-simple
|
||||
main-is: DBConnection.lhs
|
||||
|
@ -23,7 +23,7 @@ executable cookbook-db-sqlite-simple
|
|||
, http-types >= 0.12
|
||||
, markdown-unlit >= 0.4
|
||||
, http-client >= 0.5
|
||||
, sqlite-simple >= 0.4.5.0
|
||||
, sqlite-simple >= 0.4
|
||||
, transformers
|
||||
default-language: Haskell2010
|
||||
ghc-options: -Wall -pgmL markdown-unlit
|
||||
|
|
|
@ -17,7 +17,7 @@ import Control.Exception
|
|||
import Control.Monad
|
||||
import Control.Monad.IO.Class
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Network.Socket (withSocketsDo)
|
||||
import Network (withSocketsDo)
|
||||
import Network.HTTP.Client hiding (Proxy)
|
||||
import Network.HTTP.Client.MultipartFormData
|
||||
import Network.Wai.Handler.Warp
|
||||
|
@ -90,8 +90,8 @@ 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).
|
||||
servant-client this time, has servant-multipart does not
|
||||
yet have support for client generation.
|
||||
|
||||
``` haskell
|
||||
main :: IO ()
|
||||
|
@ -126,7 +126,7 @@ Content of "README.md"
|
|||
|
||||
## Getting Started
|
||||
|
||||
We have a [tutorial](http://docs.servant.dev/en/stable/tutorial/index.html) that
|
||||
We have a [tutorial](http://haskell-servant.readthedocs.org/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.
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
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
|
||||
homepage: http://haskell-servant.readthedocs.org/
|
||||
license: BSD3
|
||||
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
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.0.2, GHC==8.2.2, GHC==8.4.4, GHC==8.6.2
|
||||
|
||||
executable cookbook-file-upload
|
||||
main-is: FileUpload.lhs
|
||||
|
|
|
@ -43,13 +43,13 @@ api :: Proxy (ToServantApi Routes)
|
|||
api = genericApi (Proxy :: Proxy Routes)
|
||||
```
|
||||
|
||||
It's recommended to use `genericApi` function, as then you'll get
|
||||
It's recommented 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,
|
||||
we can get safe links very conviently. We don't need to define endpoint types,
|
||||
as field accessors work as proxies:
|
||||
|
||||
```haskell
|
||||
|
@ -67,7 +67,7 @@ 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
|
||||
Here we use `genericClientHoist` function, which let us simultaneously
|
||||
hoist the monad, in this case from `ClientM` to `IO`.
|
||||
|
||||
```haskell
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
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
|
||||
homepage: http://haskell-servant.readthedocs.org/
|
||||
license: BSD3
|
||||
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
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.0.2, GHC==8.2.2, GHC==8.4.4, GHC==8.6.2
|
||||
|
||||
executable cookbook-using-custom-monad
|
||||
main-is: Generic.lhs
|
||||
|
|
|
@ -254,7 +254,7 @@ loginHandler cookieSettings jwtSettings form = do
|
|||
liftIO $ pushLogStrLn logset $ toLogStr logMsg
|
||||
throwError err401
|
||||
Just applyCookies -> do
|
||||
let successMsg = logMsg{message = "AdminUser successfully authenticated!"}
|
||||
let successMsg = logMsg{message = "AdminUser succesfully authenticated!"}
|
||||
liftIO $ pushLogStrLn logset $ toLogStr successMsg
|
||||
pure $ applyCookies successMsg
|
||||
loginHandler _ _ _ = throwError err401
|
||||
|
@ -287,7 +287,7 @@ mkApp cfg cs jwts ctx =
|
|||
(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
|
||||
One footenote: 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
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
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
|
||||
homepage: http://haskell-servant.readthedocs.org/
|
||||
license: BSD3
|
||||
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
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.0.2, GHC==8.2.2, GHC==8.4.4, GHC==8.6.2
|
||||
|
||||
executable cookbook-hoist-server-with-context
|
||||
main-is: HoistServerWithContext.lhs
|
||||
|
@ -24,7 +24,7 @@ executable cookbook-hoist-server-with-context
|
|||
, servant
|
||||
, servant-server
|
||||
, servant-auth >= 0.3.2
|
||||
, servant-auth-server >= 0.4.4.0
|
||||
, servant-auth-server
|
||||
, time
|
||||
, warp >= 3.2
|
||||
, wai >= 3.2
|
||||
|
|
|
@ -34,16 +34,16 @@ 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)
|
||||
The [`warp-tls`](https://hackage.haskell.org/package/warp-tls-3.2.4/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).
|
||||
[`runTLS`](https://hackage.haskell.org/package/warp-tls-3.2.4/docs/Network-Wai-Handler-WarpTLS.html#v:runTLS)
|
||||
and [`runTLSSocket`](https://hackage.haskell.org/package/warp-tls-3.2.4/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)
|
||||
[the TLS settings](https://hackage.haskell.org/package/warp-tls-3.2.4/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)
|
||||
and [the warp settings](https://hackage.haskell.org/package/warp-3.2.12/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
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
cabal-version: 2.2
|
||||
name: cookbook-https
|
||||
version: 0.1
|
||||
synopsis: HTTPS cookbook example
|
||||
homepage: http://docs.servant.dev/
|
||||
license: BSD-3-Clause
|
||||
homepage: http://haskell-servant.readthedocs.org/
|
||||
license: BSD3
|
||||
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
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.0.2, GHC==8.2.2, GHC==8.4.4, GHC==8.6.2
|
||||
|
||||
executable cookbook-https
|
||||
main-is: Https.lhs
|
||||
|
@ -17,7 +17,7 @@ executable cookbook-https
|
|||
, servant-server
|
||||
, wai >= 3.2
|
||||
, warp >= 3.2
|
||||
, warp-tls >= 3.2.9
|
||||
, warp-tls >= 3.2
|
||||
, markdown-unlit >= 0.4
|
||||
default-language: Haskell2010
|
||||
ghc-options: -Wall -pgmL markdown-unlit
|
||||
|
|
|
@ -6,8 +6,8 @@ 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
|
||||
or even to just get in touch with us on the **#servant** IRC channel
|
||||
on freenode or on
|
||||
`the mailing list <https://groups.google.com/forum/#!forum/haskell-servant>`_.
|
||||
|
||||
The scope is very wide. Simple and fancy authentication schemes,
|
||||
|
@ -20,15 +20,11 @@ you name it!
|
|||
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
|
||||
|
@ -36,5 +32,3 @@ you name it!
|
|||
curl-mock/CurlMock.lhs
|
||||
sentry/Sentry.lhs
|
||||
testing/Testing.lhs
|
||||
open-id-connect/OpenIdConnect.lhs
|
||||
managed-resource/ManagedResource.lhs
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
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
|
||||
homepage: http://haskell-servant.readthedocs.org/
|
||||
license: BSD3
|
||||
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
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.0.2, GHC==8.2.2, GHC==8.4.4, GHC==8.6.2
|
||||
|
||||
executable cookbook-jwt-and-basic-auth
|
||||
main-is: JWTAndBasicAuth.lhs
|
||||
|
@ -22,7 +22,7 @@ executable cookbook-jwt-and-basic-auth
|
|||
, servant
|
||||
, servant-client
|
||||
, servant-server
|
||||
, servant-auth == 0.4.*
|
||||
, servant-auth ==0.3.*
|
||||
, servant-auth-server >= 0.3.1.0
|
||||
, warp >= 3.2
|
||||
, wai >= 3.2
|
||||
|
|
|
@ -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
|
||||
```
|
|
@ -18,7 +18,7 @@ For example: `Range: createdAt 2017-01-15T23:14:67.000Z; offset 5; order desc` i
|
|||
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
|
||||
As a response, the server may return the list of corresponding document, and augment the
|
||||
response with 3 headers:
|
||||
|
||||
- `Accept-Ranges`: A comma-separated list of fields upon which a range can be defined
|
||||
|
@ -127,7 +127,7 @@ defaultRange =
|
|||
getDefaultRange (Proxy @Color)
|
||||
```
|
||||
|
||||
Note that `getFieldValue :: Proxy "name" -> Color -> String` is the minimal complete definition
|
||||
Note that `getFieldValue :: Proxy "name" -> Color -> String` is the minimal complete definintion
|
||||
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.
|
||||
|
@ -148,7 +148,7 @@ type MyHeaders =
|
|||
```
|
||||
|
||||
`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:
|
||||
we mentionned in introduction. Expanding the alias boils down to the following:
|
||||
|
||||
``` haskell
|
||||
-- type MyHeaders =
|
||||
|
@ -165,7 +165,7 @@ not, _servant-pagination_ provides an easy way to lift a collection of resources
|
|||
#### 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`
|
||||
type we've defined above (tight 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`.
|
||||
|
@ -192,7 +192,7 @@ the format we defined, where `<field>` here can only be `name` and `<value>` mus
|
|||
- `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
|
||||
are deducted 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'`
|
||||
|
@ -219,7 +219,7 @@ The previous ranges reads as follows:
|
|||
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
|
||||
instance. Doing so will make the other helper functions more ambiguous and type annotation are
|
||||
highly likely to be needed.
|
||||
|
||||
|
||||
|
@ -235,8 +235,8 @@ instance HasPagination Color "hex" where
|
|||
#### 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
|
||||
`HasPagination` instance. However, this can be overwritten when defining the instance to provide
|
||||
your own options. This 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
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
cabal-version: 2.2
|
||||
name: cookbook-pagination
|
||||
version: 2.1
|
||||
synopsis: Pagination with Servant example
|
||||
homepage: http://docs.servant.dev/
|
||||
license: BSD-3-Clause
|
||||
homepage: http://haskell-servant.readthedocs.org/
|
||||
license: BSD3
|
||||
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
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.0.2, GHC==8.2.2, GHC==8.4.4, GHC==8.6.2
|
||||
|
||||
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
|
||||
build-depends: base >= 4.8 && <4.13
|
||||
, aeson
|
||||
, servant
|
||||
, servant-server
|
||||
|
|
|
@ -79,14 +79,14 @@ It does three things. First it initializes the service which will communicate wi
|
|||
|
||||
- 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
|
||||
- an event trasport, which generally would be `sendRecord`, an HTTPS capable trasport 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 error level (see [SentryLevel](https://hackage.haskell.org/package/raven-haskell-0.1.2.0/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`
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
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
|
||||
homepage: http://haskell-servant.readthedocs.org/
|
||||
license: BSD3
|
||||
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
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.0.2, GHC==8.2.2, GHC==8.4.4, GHC==8.6.2
|
||||
|
||||
executable cookbook-sentry
|
||||
main-is: Sentry.lhs
|
||||
|
|
|
@ -144,7 +144,7 @@ simpleAPIServer
|
|||
:: m [a]
|
||||
-> (i -> m a)
|
||||
-> (a -> m NoContent)
|
||||
-> ServerT (SimpleAPI name a i) m
|
||||
-> Server (SimpleAPI name a i) m
|
||||
simpleAPIServer listAs getA postA =
|
||||
listAs :<|> getA :<|> postA
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
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
|
||||
homepage: http://haskell-servant.readthedocs.org/
|
||||
license: BSD3
|
||||
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
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.0.2, GHC==8.2.2, GHC==8.4.4, GHC==8.6.2
|
||||
|
||||
executable cookbook-structuring-apis
|
||||
main-is: StructuringApis.lhs
|
||||
|
|
|
@ -142,31 +142,33 @@ of it and see how it responds.
|
|||
Let's write some tests:
|
||||
|
||||
```haskell
|
||||
withUserApp :: (Warp.Port -> IO ()) -> IO ()
|
||||
withUserApp :: 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
|
||||
-- we can spin up a server in another thread and kill that thread when done
|
||||
-- in an exception-safe way
|
||||
bracket (liftIO $ C.forkIO $ Warp.run 8888 userApp)
|
||||
C.killThread
|
||||
(const action)
|
||||
|
||||
|
||||
businessLogicSpec :: Spec
|
||||
businessLogicSpec =
|
||||
-- `around` will start our Server before the tests and turn it off after
|
||||
around withUserApp $ do
|
||||
around_ withUserApp $ do
|
||||
-- create a test client function
|
||||
let createUser = client (Proxy :: Proxy UserApi)
|
||||
-- create a servant-client ClientEnv
|
||||
baseUrl <- runIO $ parseBaseUrl "http://localhost"
|
||||
baseUrl <- runIO $ parseBaseUrl "http://localhost:8888"
|
||||
manager <- runIO $ newManager defaultManagerSettings
|
||||
let clientEnv port = mkClientEnv manager (baseUrl { baseUrlPort = port })
|
||||
let clientEnv = mkClientEnv manager baseUrl
|
||||
|
||||
-- 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)
|
||||
it "should create a user with a high enough ID" $ do
|
||||
result <- runClientM (createUser 50001) clientEnv
|
||||
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)
|
||||
it "will it fail with a too-small ID?" $ do
|
||||
result <- runClientM (createUser 4999) clientEnv
|
||||
result `shouldBe` (Right $ User { name = "some user", user_id = 50001})
|
||||
```
|
||||
|
||||
|
@ -232,7 +234,7 @@ clientEnv esHost esPort = do
|
|||
manager <- newManager defaultManagerSettings
|
||||
pure $ mkClientEnv manager baseUrl
|
||||
|
||||
runSearchClient :: Text -> Text -> ClientM a -> IO (Either ClientError a)
|
||||
runSearchClient :: Text -> Text -> ClientM a -> IO (Either ServantError a)
|
||||
runSearchClient esHost esPort = (clientEnv esHost esPort >>=) . runClientM
|
||||
```
|
||||
|
||||
|
@ -265,7 +267,7 @@ docServer esHost esPort = getDocById esHost esPort
|
|||
-- actions
|
||||
getDocById :: Text -> Text -> Integer -> Handler Value
|
||||
getDocById esHost esPort docId = do
|
||||
-- Our Servant Client function returns Either ClientError Value here:
|
||||
-- Our Servant Client function returns Either ServantError Value here:
|
||||
docRes <- liftIO $ runSearchClient esHost esPort (getDocument docId)
|
||||
case docRes of
|
||||
Left err -> throwError $ err404 { errBody = "Failed looking up content" }
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
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
|
||||
homepage: http://haskell-servant.readthedocs.org/
|
||||
license: BSD3
|
||||
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
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.0.2, GHC==8.2.2, GHC==8.4.4, GHC==8.6.2
|
||||
|
||||
executable cookbook-testing
|
||||
main-is: Testing.lhs
|
||||
|
@ -23,7 +23,7 @@ executable cookbook-testing
|
|||
, servant
|
||||
, servant-client
|
||||
, servant-server
|
||||
, servant-quickcheck >= 0.0.10
|
||||
, servant-quickcheck
|
||||
, http-client
|
||||
, http-types >= 0.12
|
||||
, hspec
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Using a custom monad
|
||||
|
||||
In this section we will create an API for a book shelf without any backing DB storage.
|
||||
In this section we will create and 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:
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
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
|
||||
homepage: http://haskell-servant.readthedocs.org/
|
||||
license: BSD3
|
||||
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
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.0.2, GHC==8.2.2, GHC==8.4.4, GHC==8.6.2
|
||||
|
||||
executable cookbook-using-custom-monad
|
||||
main-is: UsingCustomMonad.lhs
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
or simply put: _a practical introduction to `Servant.Client.Free`_.
|
||||
|
||||
Someone asked on IRC how one could access the intermediate Requests (resp. Responses)
|
||||
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.
|
||||
|
@ -99,6 +99,8 @@ test = case getSquare 42 of
|
|||
putStrLn $ "ERROR: got pure result: " ++ show n
|
||||
Free (Throw err) ->
|
||||
putStrLn $ "ERROR: got error right away: " ++ show err
|
||||
Free (StreamingRequest _req _k) ->
|
||||
putStrLn $ "ERROR: need to do streaming request" -- TODO: no Show Req :(
|
||||
```
|
||||
|
||||
We are interested in `RunRequest`, that's what client should block on:
|
||||
|
@ -119,7 +121,7 @@ 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
|
||||
let req' = I.requestToClientRequest burl req
|
||||
putStrLn $ "Making request: " ++ show req'
|
||||
```
|
||||
|
||||
|
@ -135,13 +137,13 @@ 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'
|
||||
|
||||
let res = I.clientResponseToResponse res'
|
||||
|
||||
case k res of
|
||||
Pure n ->
|
||||
putStrLn $ "Expected 1764, got " ++ show n
|
||||
_ ->
|
||||
putStrLn "ERROR: didn't get a response"
|
||||
_ ->
|
||||
putStrLn "ERROR: didn't got a response"
|
||||
```
|
||||
|
||||
So that's it. Using `Free` we can evaluate servant clients step-by-step, and
|
||||
|
@ -153,7 +155,7 @@ 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
|
||||
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:
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
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
|
||||
homepage: http://haskell-servant.readthedocs.org/
|
||||
license: BSD3
|
||||
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
|
||||
cabal-version: >=1.10
|
||||
tested-with: GHC==8.0.2, GHC==8.2.2, GHC==8.4.4, GHC==8.6.2
|
||||
|
||||
executable cookbook-using-free-client
|
||||
main-is: UsingFreeClient.lhs
|
||||
|
|
|
@ -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
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
- **[servant-examples](https://github.com/sras/servant-examples)**:
|
||||
|
||||
Similar to [the cookbook](https://docs.servant.dev/en/latest/cookbook/index.html) but
|
||||
Similar to [the cookbook](https://haskell-servant.readthedocs.io/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.
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ Helpful Links
|
|||
-------------
|
||||
|
||||
- the central documentation (this site):
|
||||
`docs.servant.dev <http://docs.servant.dev/>`_
|
||||
`haskell-servant.readthedocs.org <http://haskell-servant.readthedocs.org/>`_
|
||||
|
||||
- the github repo:
|
||||
`github.com/haskell-servant/servant <https://github.com/haskell-servant/servant>`_
|
||||
|
@ -12,13 +12,13 @@ Helpful Links
|
|||
`https://github.com/haskell-servant/servant/issues <https://github.com/haskell-servant/servant/issues>`_
|
||||
|
||||
- the irc channel:
|
||||
`#haskell-servant on libera.chat <https://web.libera.chat/#haskell-servant>`_
|
||||
``#servant`` on freenode
|
||||
|
||||
- the mailing list:
|
||||
`groups.google.com/forum/#!forum/haskell-servant <https://groups.google.com/forum/#!forum/haskell-servant>`_
|
||||
|
||||
- blog posts and videos and slides of some talks on servant:
|
||||
`www.servant.dev <http://www.servant.dev>`_
|
||||
`haskell-servant.github.io <http://haskell-servant.github.io>`_
|
||||
|
||||
- the servant packages on hackage:
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
recommonmark==0.5.0
|
||||
Sphinx==1.8.4
|
||||
sphinx_rtd_theme>=0.4.2
|
||||
jinja2<3.1.0
|
||||
recommonmark==0.4.0
|
||||
Sphinx==1.7.5
|
||||
sphinx_rtd_theme>=0.4.0
|
||||
|
|
|
@ -177,12 +177,13 @@ type UserAPI5 = "user" :> Capture "userid" Integer :> Get '[JSON] User
|
|||
-- except that we explicitly say that "userid"
|
||||
-- must be an integer
|
||||
|
||||
:<|> "user" :> Capture "userid" Integer :> DeleteNoContent
|
||||
:<|> "user" :> Capture "userid" Integer :> DeleteNoContent '[JSON] NoContent
|
||||
-- equivalent to 'DELETE /user/:userid'
|
||||
```
|
||||
|
||||
In the second case, `DeleteNoContent` specifies a 204 response code
|
||||
and that the response will always be empty.
|
||||
In the second case, `DeleteNoContent` specifies a 204 response code,
|
||||
`JSON` specifies the content types on which the handler will match,
|
||||
and `NoContent` says that the response will always be empty.
|
||||
|
||||
### `QueryParam`, `QueryParams`, `QueryFlag`
|
||||
|
||||
|
@ -389,30 +390,3 @@ One example for this is if you want to serve a directory of static files along
|
|||
with the rest of your API. But you can plug in everything that is an
|
||||
`Application`, e.g. a whole web application written in any of the web
|
||||
frameworks that support `wai`.
|
||||
|
||||
Be mindful! The `servant-server`'s router works by pattern-matching the
|
||||
different routes that are composed using `:<|>`. `Raw`, as an escape hatch,
|
||||
matches any route that hasn't been matched by previous patterns. Therefore,
|
||||
any subsequent route will be silently ignored.
|
||||
|
||||
``` haskell
|
||||
type UserAPI14 = Raw
|
||||
:<|> "users" :> Get '[JSON] [User]
|
||||
-- In this situation, the /users endpoint
|
||||
-- will not be reachable because the Raw
|
||||
-- endpoint matches requests before
|
||||
```
|
||||
A simple way to avoid this pitfall is to either use `Raw` as the last
|
||||
definition, or to always have it under a static path.
|
||||
|
||||
``` haskell
|
||||
type UserAPI15 = "files" :> Raw
|
||||
-- The raw endpoint is under the /files
|
||||
-- static path, so it won't match /users.
|
||||
:<|> "users" :> Get '[JSON] [User]
|
||||
|
||||
type UserAPI16 = "users" :> Get '[JSON] [User]
|
||||
:<|> Raw
|
||||
-- The Raw endpoint is matched last, so
|
||||
-- it won't overlap another endpoint.
|
||||
```
|
||||
|
|
|
@ -47,6 +47,7 @@ module Authentication where
|
|||
import Data.Aeson (ToJSON)
|
||||
import Data.ByteString (ByteString)
|
||||
import Data.Map (Map, fromList)
|
||||
import Data.Monoid ((<>))
|
||||
import qualified Data.Map as Map
|
||||
import Data.Proxy (Proxy (Proxy))
|
||||
import Data.Text (Text)
|
||||
|
@ -107,7 +108,7 @@ API with "private." Additionally, the private parts of our API use the
|
|||
realm for this authentication is `"foo-realm"`).
|
||||
|
||||
Unfortunately we're not done. When someone makes a request to our `"private"`
|
||||
API, we're going to need to provide to servant the logic for validating
|
||||
API, we're going to need to provide to servant the logic for validifying
|
||||
usernames and passwords. This adds a certain conceptual wrinkle in servant's
|
||||
design that we'll briefly discuss. If you want the **TL;DR**: we supply a lookup
|
||||
function to servant's new `Context` primitive.
|
||||
|
@ -132,7 +133,7 @@ combinator. Using `Context`, we can supply a function of type
|
|||
handler. This will allow the handler to check authentication and return a `User`
|
||||
to downstream handlers if successful.
|
||||
|
||||
In practice we wrap `BasicAuthData -> Handler User` into a slightly
|
||||
In practice we wrap `BasicAuthData -> Handler` into a slightly
|
||||
different function to better capture the semantics of basic authentication:
|
||||
|
||||
``` haskell ignore
|
||||
|
@ -259,7 +260,7 @@ this.
|
|||
|
||||
Let's implement a trivial authentication scheme. We will protect our API by
|
||||
looking for a cookie named `"servant-auth-cookie"`. This cookie's value will
|
||||
contain a key from which we can lookup an `Account`.
|
||||
contain a key from which we can lookup a `Account`.
|
||||
|
||||
```haskell
|
||||
-- | An account type that we "fetch from the database" after
|
||||
|
@ -273,7 +274,7 @@ database = fromList [ ("key1", Account "Anne Briggs")
|
|||
, ("key3", Account "Ghédalia Tazartès")
|
||||
]
|
||||
|
||||
-- | A method that, when given a password, will return an Account.
|
||||
-- | A method that, when given a password, will return a Account.
|
||||
-- This is our bespoke (and bad) authentication logic.
|
||||
lookupAccount :: ByteString -> Handler Account
|
||||
lookupAccount key = case Map.lookup key database of
|
||||
|
@ -317,7 +318,7 @@ genAuthAPI = Proxy
|
|||
|
||||
Now we need to bring everything together for the server. We have the
|
||||
`AuthHandler Request Account` value and an `AuthProtected` endpoint. To bind these
|
||||
together, we need to provide a [Type Family](https://downloads.haskell.org/~ghc/8.8.1/docs/html/users_guide/glasgow_exts.html#type-families)
|
||||
together, we need to provide a [Type Family](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/type-families.html)
|
||||
instance that tells the `HasServer` instance that our `Context` will supply a
|
||||
`Account` (via `AuthHandler Request Account`) and that downstream combinators will
|
||||
have access to this `Account` value (or an error will be thrown if authentication
|
||||
|
@ -345,7 +346,7 @@ genAuthServerContext = authHandler :. EmptyContext
|
|||
|
||||
-- | Our API, where we provide all the author-supplied handlers for each end
|
||||
-- point. Note that 'privateDataFunc' is a function that takes 'Account' as an
|
||||
-- argument. We don't worry about the authentication instrumentation here,
|
||||
-- argument. We dont' worry about the authentication instrumentation here,
|
||||
-- that is taken care of by supplying context
|
||||
genAuthServer :: Server AuthGenAPI
|
||||
genAuthServer =
|
||||
|
@ -367,10 +368,10 @@ genAuthMain = run 8080 (serveWithContext genAuthAPI genAuthServerContext genAuth
|
|||
$ curl -XGET localhost:8080/private
|
||||
Missing auth header
|
||||
|
||||
$ curl -XGET localhost:8080/private -H "Cookie: servant-auth-cookie=key3"
|
||||
$ curl -XGET localhost:8080/private -H "servant-auth-cookie: key3"
|
||||
[{"ssshhh":"this is a secret: Ghédalia Tazartès"}]
|
||||
|
||||
$ curl -XGET localhost:8080/private -H "Cookie: servant-auth-cookie=bad-key"
|
||||
$ curl -XGET localhost:8080/private -H "servant-auth-cookie: bad-key"
|
||||
Invalid Cookie
|
||||
|
||||
$ curl -XGET localhost:8080/public
|
||||
|
@ -384,11 +385,11 @@ 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
|
||||
2. choose a 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.
|
||||
will be executed everytime a request matches a protected route.
|
||||
4. Provide an instance of the `AuthServerData` type family, specifying your
|
||||
application-specific data type returned when authentication is successful (in
|
||||
our case this was `Account`).
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
# Querying an API
|
||||
|
||||
While defining handlers that [serve an API](Server.html) has a lot to it, querying an API is simpler: we do not care about what happens inside the webserver, we just need to know how to talk to it and get a response back. That said, we usually have to write the querying functions by hand because the structure of the API isn't a first class citizen and can't be inspected to generate the client-side functions.
|
||||
While defining handlers that [serve an API](Server.lhs) has a lot to it, querying an API is simpler: we do not care about what happens inside the webserver, we just need to know how to talk to it and get a response back. That said, we usually have to write the querying functions by hand because the structure of the API isn't a first class citizen and can't be inspected to generate the client-side functions.
|
||||
|
||||
**servant** however has a way to inspect APIs, because APIs are just Haskell types and (GHC) Haskell lets us do quite a few things with types. In the same way that we look at an API type to deduce the types the handlers should have, we can inspect the structure of the API to *derive* Haskell functions that take one argument for each occurrence of `Capture`, `ReqBody`, `QueryParam`
|
||||
and friends (see [the tutorial introduction](ApiType.html) for an overview). By *derive*, we mean that there's no code generation involved - the functions are defined just by the structure of the API type.
|
||||
and friends (see [the tutorial introduction](ApiType.lhs) for an overview). By *derive*, we mean that there's no code generation involved - the functions are defined just by the structure of the API type.
|
||||
|
||||
The source for this tutorial section is a literate Haskell file, so first we need to have some language extensions and imports:
|
||||
|
||||
|
@ -161,7 +161,7 @@ The types of the arguments for the functions are the same as for (server-side) r
|
|||
## Changing the monad the client functions live in
|
||||
|
||||
Just like `hoistServer` allows us to change the monad in which request handlers
|
||||
of a web application live, we also have `hoistClient` for changing the monad
|
||||
of a web application live in, we also have `hoistClient` for changing the monad
|
||||
in which _client functions_ live. Consider the following trivial API:
|
||||
|
||||
``` haskell
|
||||
|
@ -173,7 +173,7 @@ hoistClientAPI = Proxy
|
|||
|
||||
We already know how to derive client functions for this API, and as we have
|
||||
seen above they all return results in the `ClientM` monad when using `servant-client`.
|
||||
However, `ClientM` is rarely (or never) the actual monad we need to use the client
|
||||
However, `ClientM` rarely (or never) is the actual monad we need to use the client
|
||||
functions in. Sometimes we need to run them in IO, sometimes in a custom monad
|
||||
stack. `hoistClient` is a very simple solution to the problem of "changing" the monad
|
||||
the clients run in.
|
||||
|
|
|
@ -77,7 +77,7 @@ instance ToSample HelloMessage where
|
|||
[ ("When a value is provided for 'name'", HelloMessage "Hello, Alp")
|
||||
, ("When 'name' is not specified", HelloMessage "Hello, anonymous coward")
|
||||
]
|
||||
-- multiple examples to display this time
|
||||
-- mutliple examples to display this time
|
||||
|
||||
ci :: ClientInfo
|
||||
ci = ClientInfo "Alp" "alp@foo.com" 26 ["haskell", "mathematics"]
|
||||
|
@ -108,7 +108,7 @@ apiDocs = docs exampleAPI
|
|||
markdown :: API -> String
|
||||
```
|
||||
|
||||
That lets us see what our API docs look like in markdown, by looking at `markdown apiDocs`.
|
||||
That lets us see what our API docs look down in markdown, by looking at `markdown apiDocs`.
|
||||
|
||||
````````` text
|
||||
## GET /hello
|
||||
|
|
|
@ -228,13 +228,13 @@ data CommonGeneratorOptions = CommonGeneratorOptions
|
|||
{
|
||||
-- | function generating function names
|
||||
functionNameBuilder :: FunctionName -> Text
|
||||
-- | name used when a user wants to send the request body (to let you redefine it)
|
||||
-- | name used when a user want to send the request body (to let you redefine it)
|
||||
, requestBody :: Text
|
||||
-- | name of the callback parameter when the request was successful
|
||||
, successCallback :: Text
|
||||
-- | name of the callback parameter when the request reported an error
|
||||
, errorCallback :: Text
|
||||
-- | namespace on which we define the js function (empty means local var)
|
||||
-- | namespace on which we define the js function (empty mean local var)
|
||||
, moduleName :: Text
|
||||
-- | a prefix that should be prepended to the URL in the generated JS
|
||||
, urlPrefix :: Text
|
||||
|
|
|
@ -183,7 +183,7 @@ users2 = [isaac, albert]
|
|||
|
||||
Now, just like we separate the various endpoints in `UserAPI` with `:<|>`, we
|
||||
are going to separate the handlers with `:<|>` too! They must be provided in
|
||||
the same order as in the API type.
|
||||
the same order as in in the API type.
|
||||
|
||||
``` haskell
|
||||
server2 :: Server UserAPI2
|
||||
|
@ -313,8 +313,8 @@ For reference, here's a list of some combinators from **servant**:
|
|||
## The `FromHttpApiData`/`ToHttpApiData` classes
|
||||
|
||||
Wait... How does **servant** know how to decode the `Int`s from the URL? Or how
|
||||
to decode a `ClientInfo` value from the request body? The following three sections will
|
||||
help us answer these questions.
|
||||
to decode a `ClientInfo` value from the request body? This is what this and the
|
||||
following two sections address.
|
||||
|
||||
`Capture`s and `QueryParam`s are represented by some textual value in URLs.
|
||||
`Header`s are similarly represented by a pair of a header name and a
|
||||
|
@ -599,7 +599,7 @@ $ curl -H 'Accept: text/html' http://localhost:8081/persons
|
|||
|
||||
## The `Handler` monad
|
||||
|
||||
At the heart of the handlers is the monad they run in, namely a newtype `Handler` around `ExceptT ServerError IO`
|
||||
At the heart of the handlers is the monad they run in, namely a newtype `Handler` around `ExceptT ServantErr IO`
|
||||
([haddock documentation for `ExceptT`](http://hackage.haskell.org/package/mtl-2.2.1/docs/Control-Monad-Except.html#t:ExceptT)).
|
||||
One might wonder: why this monad? The answer is that it is the
|
||||
simplest monad with the following properties:
|
||||
|
@ -617,12 +617,12 @@ newtype ExceptT e m a = ExceptT (m (Either e a))
|
|||
```
|
||||
|
||||
In short, this means that a handler of type `Handler a` is simply
|
||||
equivalent to a computation of type `IO (Either ServerError a)`, that is, an IO
|
||||
equivalent to a computation of type `IO (Either ServantErr a)`, that is, an IO
|
||||
action that either returns an error or a result.
|
||||
|
||||
The module [`Control.Monad.Except`](https://hackage.haskell.org/package/mtl/docs/Control-Monad-Except.html#t:ExceptT)
|
||||
The module [`Control.Monad.Except`](https://hackage.haskell.org/package/mtl-2.2.1/docs/Control-Monad-Except.html#t:ExceptT)
|
||||
from which `ExceptT` comes is worth looking at.
|
||||
Perhaps most importantly, `ExceptT` and `Handler` are instances of `MonadError`, so
|
||||
Perhaps most importantly, `ExceptT` and `Handler` are an instances of `MonadError`, so
|
||||
`throwError` can be used to return an error from your handler (whereas `return`
|
||||
is enough to return a success).
|
||||
|
||||
|
@ -632,9 +632,9 @@ kind and abort early. The next two sections cover how to do just that.
|
|||
|
||||
### Performing IO
|
||||
|
||||
Other important instances from the list above are `MonadIO m => MonadIO
|
||||
(ExceptT e m)`, and therefore also `MonadIO Handler` as there is a `MonadIO IO` instance.
|
||||
[`MonadIO`](http://hackage.haskell.org/package/base/docs/Control-Monad-IO-Class.html#t:MonadIO)
|
||||
Another important instances from the list above are `MonadIO m => MonadIO
|
||||
(ExceptT e m)`, and therefore also `MonadIO Handler` as there is `MonadIO IO` instance.
|
||||
[`MonadIO`](http://hackage.haskell.org/package/transformers-0.4.3.0/docs/Control-Monad-IO-Class.html)
|
||||
is a class from the **transformers** package defined as:
|
||||
|
||||
``` haskell ignore
|
||||
|
@ -660,16 +660,16 @@ server5 = do
|
|||
return (FileContent filecontent)
|
||||
```
|
||||
|
||||
### Failing, through `ServerError`
|
||||
### Failing, through `ServantErr`
|
||||
|
||||
If you want to explicitly fail at providing the result promised by an endpoint
|
||||
using the appropriate HTTP status code (not found, unauthorized, etc) and some
|
||||
error message, all you have to do is use the `throwError` function mentioned above
|
||||
and provide it with the appropriate value of type `ServerError`, which is
|
||||
and provide it with the appropriate value of type `ServantErr`, which is
|
||||
defined as:
|
||||
|
||||
``` haskell ignore
|
||||
data ServerError = ServerError
|
||||
data ServantErr = ServantErr
|
||||
{ errHTTPCode :: Int
|
||||
, errReasonPhrase :: String
|
||||
, errBody :: ByteString -- lazy bytestring
|
||||
|
@ -685,7 +685,7 @@ use record update syntax:
|
|||
failingHandler :: Handler ()
|
||||
failingHandler = throwError myerr
|
||||
|
||||
where myerr :: ServerError
|
||||
where myerr :: ServantErr
|
||||
myerr = err503 { errBody = "Sorry dear user." }
|
||||
```
|
||||
|
||||
|
@ -716,7 +716,7 @@ $ curl --verbose http://localhost:8081/myfile.txt
|
|||
>
|
||||
< HTTP/1.1 404 Not Found
|
||||
[snip]
|
||||
myfile.txt just isn't there, please leave this server alone.
|
||||
myfile.txt just isnt there, please leave this server alone.
|
||||
|
||||
$ echo Hello > myfile.txt
|
||||
|
||||
|
@ -818,7 +818,7 @@ If it doesn't exist, the handler will fail with a `404` status code.
|
|||
|
||||
`serveDirectoryWebApp` uses some standard settings that fit the use case of
|
||||
serving static files for most web apps. You can find out about the other
|
||||
options in the documentation of the `Servant.Server.StaticFiles` module.
|
||||
options in the documentation of the `Servant.Utils.StaticFiles` module.
|
||||
|
||||
## Nested APIs
|
||||
|
||||
|
@ -830,7 +830,7 @@ type UserAPI3 = -- view the user with given userid, in JSON
|
|||
Capture "userid" Int :> Get '[JSON] User
|
||||
|
||||
:<|> -- delete the user with given userid. empty response
|
||||
Capture "userid" Int :> DeleteNoContent
|
||||
Capture "userid" Int :> DeleteNoContent '[JSON] NoContent
|
||||
```
|
||||
|
||||
We can instead factor out the `userid`:
|
||||
|
@ -838,7 +838,7 @@ We can instead factor out the `userid`:
|
|||
``` haskell
|
||||
type UserAPI4 = Capture "userid" Int :>
|
||||
( Get '[JSON] User
|
||||
:<|> DeleteNoContent
|
||||
:<|> DeleteNoContent '[JSON] NoContent
|
||||
)
|
||||
```
|
||||
|
||||
|
@ -896,13 +896,13 @@ type API1 = "users" :>
|
|||
-- we factor out the Request Body
|
||||
type API2 = ReqBody '[JSON] User :>
|
||||
( Get '[JSON] User -- just display the same user back, don't register it
|
||||
:<|> PostNoContent -- register the user. empty response
|
||||
:<|> PostNoContent '[JSON] NoContent -- register the user. empty response
|
||||
)
|
||||
|
||||
-- we factor out a Header
|
||||
type API3 = Header "Authorization" Token :>
|
||||
( Get '[JSON] SecretData -- get some secret data, if authorized
|
||||
:<|> ReqBody '[JSON] SecretData :> PostNoContent -- add some secret data, if authorized
|
||||
:<|> ReqBody '[JSON] SecretData :> PostNoContent '[JSON] NoContent -- add some secret data, if authorized
|
||||
)
|
||||
|
||||
newtype Token = Token ByteString
|
||||
|
@ -915,11 +915,11 @@ API type only at the end.
|
|||
``` haskell
|
||||
type UsersAPI =
|
||||
Get '[JSON] [User] -- list users
|
||||
:<|> ReqBody '[JSON] User :> PostNoContent -- add a user
|
||||
:<|> ReqBody '[JSON] User :> PostNoContent '[JSON] NoContent -- add a user
|
||||
:<|> Capture "userid" Int :>
|
||||
( Get '[JSON] User -- view a user
|
||||
:<|> ReqBody '[JSON] User :> PutNoContent -- update a user
|
||||
:<|> DeleteNoContent -- delete a user
|
||||
:<|> ReqBody '[JSON] User :> PutNoContent '[JSON] NoContent -- update a user
|
||||
:<|> DeleteNoContent '[JSON] NoContent -- delete a user
|
||||
)
|
||||
|
||||
usersServer :: Server UsersAPI
|
||||
|
@ -948,11 +948,11 @@ usersServer = getUsers :<|> newUser :<|> userOperations
|
|||
``` haskell
|
||||
type ProductsAPI =
|
||||
Get '[JSON] [Product] -- list products
|
||||
:<|> ReqBody '[JSON] Product :> PostNoContent -- add a product
|
||||
:<|> ReqBody '[JSON] Product :> PostNoContent '[JSON] NoContent -- add a product
|
||||
:<|> Capture "productid" Int :>
|
||||
( Get '[JSON] Product -- view a product
|
||||
:<|> ReqBody '[JSON] Product :> PutNoContent -- update a product
|
||||
:<|> DeleteNoContent -- delete a product
|
||||
:<|> ReqBody '[JSON] Product :> PutNoContent '[JSON] NoContent -- update a product
|
||||
:<|> DeleteNoContent '[JSON] NoContent -- delete a product
|
||||
)
|
||||
|
||||
data Product = Product { productId :: Int }
|
||||
|
@ -996,11 +996,11 @@ abstract that away:
|
|||
-- indexed by values of type 'i'
|
||||
type APIFor a i =
|
||||
Get '[JSON] [a] -- list 'a's
|
||||
:<|> ReqBody '[JSON] a :> PostNoContent -- add an 'a'
|
||||
:<|> ReqBody '[JSON] a :> PostNoContent '[JSON] NoContent -- add an 'a'
|
||||
:<|> Capture "id" i :>
|
||||
( Get '[JSON] a -- view an 'a' given its "identifier" of type 'i'
|
||||
:<|> ReqBody '[JSON] a :> PutNoContent -- update an 'a'
|
||||
:<|> DeleteNoContent -- delete an 'a'
|
||||
:<|> ReqBody '[JSON] a :> PutNoContent '[JSON] NoContent -- update an 'a'
|
||||
:<|> DeleteNoContent '[JSON] NoContent -- delete an 'a'
|
||||
)
|
||||
|
||||
-- Build the appropriate 'Server'
|
||||
|
@ -1128,14 +1128,14 @@ This is the webservice in action:
|
|||
``` bash
|
||||
$ curl http://localhost:8081/a
|
||||
1797
|
||||
$ curl http://localhost:8081/b -X GET -d '42.0' -H 'Content-Type: application/json'
|
||||
true
|
||||
$ curl http://localhost:8081/b
|
||||
"hi"
|
||||
```
|
||||
|
||||
### An arrow is a reader too.
|
||||
|
||||
In previous versions of `servant` we had an `enter` to do what `hoistServer`
|
||||
does now. `enter` had an ambitious design goals, but was problematic in practice.
|
||||
does now. `enter` had a ambitious design goals, but was problematic in practice.
|
||||
|
||||
One problematic situation was when the source monad was `(->) r`, yet it's
|
||||
handy in practice, because `(->) r` is isomorphic to `Reader r`.
|
||||
|
@ -1166,7 +1166,7 @@ back a *stream* of results, served one at a time. Stream endpoints only provide
|
|||
a single content type, and also specify what framing strategy is used to
|
||||
delineate the results. To serve these results, we need to give back a stream
|
||||
producer. Adapters can be written to *Pipes*, *Conduit* and the like, or
|
||||
written directly as `SourceIO`s. SourceIO builds upon servant's own `SourceT`
|
||||
written directly as `SourceIO`s. SourceIO builts upon servant's own `SourceT`
|
||||
stream type (it's simpler than *Pipes* or *Conduit*).
|
||||
The API of a streaming endpoint needs to explicitly specify which sort of
|
||||
generator it produces. Note that the generator itself is returned by a
|
||||
|
|
|
@ -6,10 +6,29 @@ This is an introductory tutorial to **servant**. Whilst browsing is fine, it mak
|
|||
Any comments, issues or feedback about the tutorial can be submitted
|
||||
to `servant's issue tracker <http://github.com/haskell-servant/servant/issues>`_.
|
||||
|
||||
In fact, the whole tutorial is a `cabal <https://cabal.readthedocs.io/en/latest/>`_
|
||||
project and can be built and played with locally as follows:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ git clone https://github.com/haskell-servant/servant.git
|
||||
$ cd servant
|
||||
# build
|
||||
$ cabal new-build tutorial
|
||||
# load in ghci to play with it
|
||||
$ cabal new-repl tutorial
|
||||
|
||||
The code can be found in the `*.lhs` files under `doc/tutorial/` in the
|
||||
repository. Feel free to edit it while you're reading this documentation and
|
||||
see the effect of your changes.
|
||||
|
||||
`Nix <https://nixos.org/nix/>`_ users should feel free to take a look at
|
||||
the `nix/shell.nix` file in the repository and use it to provision a suitable
|
||||
environment to build and run the examples.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
install.rst
|
||||
ApiType.lhs
|
||||
Server.lhs
|
||||
Client.lhs
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
Install
|
||||
========
|
||||
|
||||
cabal-install
|
||||
--------
|
||||
|
||||
The whole tutorial is a `cabal <https://cabal.readthedocs.io/en/latest/>`_
|
||||
project and can be built locally as follows:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ git clone https://github.com/haskell-servant/servant.git
|
||||
$ cd servant
|
||||
# build
|
||||
$ cabal new-build tutorial
|
||||
# load in ghci to play with it
|
||||
$ cabal new-repl tutorial
|
||||
|
||||
stack
|
||||
--------
|
||||
|
||||
The servant `stack <https://docs.haskellstack.org/en/stable/README/>`_ template includes the working tutorial. To initialize this template, run:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ stack new myproj servant
|
||||
$ cd myproj
|
||||
# build
|
||||
$ stack build
|
||||
# start server
|
||||
$ stack exec myproj-exe
|
||||
|
||||
The code can be found in the `*.lhs` files under `doc/tutorial/` in the
|
||||
repository. Feel free to edit it while you're reading this documentation and
|
||||
see the effect of your changes.
|
||||
|
||||
nix
|
||||
--------
|
||||
|
||||
`Nix <https://nixos.org/nix/>`_ users should feel free to take a look at
|
||||
the `nix/shell.nix` file in the repository and use it to provision a suitable
|
||||
environment to build and run the examples.
|
||||
|
||||
Note for Ubuntu users
|
||||
--------
|
||||
|
||||
Ubuntu's packages for `ghc`, `cabal`, and `stack` are years out of date.
|
||||
If the instructions above fail for you,
|
||||
try replacing the Ubuntu packages with up-to-date versions.
|
||||
First remove the installed versions:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# remove the obsolete versions
|
||||
$ sudo apt remove ghc haskell-stack cabal-install
|
||||
|
||||
Then install fresh versions of the Haskell toolchain
|
||||
using the `ghcup <https://www.haskell.org/ghcup/install/>`_ installer.
|
||||
|
||||
As of February 2022, one easy way to do this is by running a bootstrap script:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
|
||||
|
||||
The script is interactive and will prompt you for details about what
|
||||
you want installed and where. To install manually,
|
||||
see `the detailed instructions <https://www.haskell.org/ghcup/install/#manual-install>`_.
|
|
@ -1,11 +1 @@
|
|||
module Main where
|
||||
|
||||
import qualified JavascriptSpec
|
||||
|
||||
import Test.Hspec (Spec, hspec, describe)
|
||||
|
||||
main :: IO ()
|
||||
main = hspec spec
|
||||
|
||||
spec :: Spec
|
||||
spec = describe "Javascript" JavascriptSpec.spec
|
||||
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
cabal-version: 2.2
|
||||
name: tutorial
|
||||
version: 0.10
|
||||
synopsis: The servant tutorial
|
||||
description:
|
||||
The servant tutorial can be found at
|
||||
<http://docs.servant.dev/>
|
||||
homepage: http://docs.servant.dev/
|
||||
<http://haskell-servant.readthedocs.org/>
|
||||
homepage: http://haskell-servant.readthedocs.org/
|
||||
category: Servant, Documentation
|
||||
license: BSD-3-Clause
|
||||
license: BSD3
|
||||
license-file: LICENSE
|
||||
author: Servant Contributors
|
||||
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
tested-with:
|
||||
GHC==8.6.5
|
||||
GHC==8.8.3, GHC ==8.10.7
|
||||
GHC==8.0.2
|
||||
GHC==8.2.2
|
||||
GHC==8.4.4
|
||||
GHC==8.6.2
|
||||
extra-source-files:
|
||||
static/index.html
|
||||
static/ui.js
|
||||
|
@ -32,7 +34,7 @@ library
|
|||
-- Packages `servant` depends on.
|
||||
-- We don't need to specify bounds here as this package is never released.
|
||||
build-depends:
|
||||
base >= 4.7 && <5
|
||||
base >= 4.7 && <4.13
|
||||
, aeson
|
||||
, attoparsec
|
||||
, base-compat
|
||||
|
@ -63,11 +65,11 @@ library
|
|||
blaze-html >= 0.9.0.1 && < 0.10
|
||||
, blaze-markup >= 0.8.0.0 && < 0.9
|
||||
, cookie >= 0.4.3 && < 0.5
|
||||
, js-jquery >= 3.3.1 && < 3.4
|
||||
, lucid >= 2.9.11 && < 2.12
|
||||
, random >= 1.1 && < 1.3
|
||||
, js-jquery >= 3.2.1 && < 3.3
|
||||
, lucid >= 2.9.11 && < 2.10
|
||||
, random >= 1.1 && < 1.2
|
||||
, servant-js >= 0.9 && < 0.10
|
||||
, time >= 1.6.0.1 && < 1.13
|
||||
, time >= 1.6.0.1 && < 1.9
|
||||
|
||||
-- For legacy tools, we need to specify build-depends too
|
||||
build-depends: markdown-unlit >= 0.5.0 && <0.6
|
||||
|
@ -80,6 +82,8 @@ test-suite spec
|
|||
hs-source-dirs: test
|
||||
main-is: Spec.hs
|
||||
other-modules: JavascriptSpec
|
||||
build-tool-depends:
|
||||
hspec-discover:hspec-discover
|
||||
build-depends: base
|
||||
, tutorial
|
||||
, hspec
|
||||
|
|
22
ghcjs.nix
22
ghcjs.nix
|
@ -1,22 +0,0 @@
|
|||
let reflex-platform = import (builtins.fetchTarball
|
||||
{ name = "reflex-platform";
|
||||
url = "https://github.com/reflex-frp/reflex-platform/archive/1aba6f367982bd6dd78ec2fda75ab246a62d32c5.tar.gz";
|
||||
}) {};
|
||||
pkgs = import ./nix/nixpkgs.nix; in
|
||||
|
||||
pkgs.stdenv.mkDerivation {
|
||||
name = "ghcjs-shell";
|
||||
buildInputs =
|
||||
[ (reflex-platform.ghcjs.ghcWithPackages (p: with p; [
|
||||
attoparsec
|
||||
hashable
|
||||
]))
|
||||
pkgs.cabal-install
|
||||
pkgs.gmp
|
||||
pkgs.haskellPackages.cabal-plan
|
||||
pkgs.haskellPackages.hspec-discover
|
||||
pkgs.nodejs
|
||||
pkgs.perl
|
||||
pkgs.zlib
|
||||
];
|
||||
}
|
|
@ -21,21 +21,3 @@ a particular ghc version, e.g:
|
|||
``` sh
|
||||
$ nix-shell nix/shell.nix --argstr compiler ghcHEAD
|
||||
```
|
||||
|
||||
**Possible GHC versions**
|
||||
- `ghc865Binary`
|
||||
- `ghc884`
|
||||
- `ghc8104` - default
|
||||
- `ghc901`
|
||||
|
||||
### Cabal users
|
||||
|
||||
GHC version can be chosen via the nix-shell parameter
|
||||
|
||||
`cabal build all`
|
||||
|
||||
### Stack version
|
||||
|
||||
Since the ghc version is set by the LTS version, it is preferable to use the `ghc8104` version parameter for the nix-shell.
|
||||
|
||||
`stack --no-nix --system-ghc <command>`
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"rev" : "05f0934825c2a0750d4888c4735f9420c906b388",
|
||||
"sha256" : "1g8c2w0661qn89ajp44znmwfmghbbiygvdzq0rzlvlpdiz28v6gy"
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
import (builtins.fetchTarball {
|
||||
url = "https://github.com/NixOS/nixpkgs/archive/refs/tags/21.05.tar.gz";
|
||||
sha256 = "sha256:1ckzhh24mgz6jd1xhfgx0i9mijk6xjqxwsshnvq789xsavrmsc36";
|
||||
}) {}
|
|
@ -1,20 +1,21 @@
|
|||
{ compiler ? "ghc8104"
|
||||
{ pkgs ? import <nixpkgs> {}
|
||||
, compiler ? "ghc822"
|
||||
, tutorial ? false
|
||||
, pkgs ? import ./nixpkgs.nix
|
||||
}:
|
||||
|
||||
with pkgs;
|
||||
|
||||
let
|
||||
ghc = haskell.packages.${compiler}.ghcWithPackages (_: []);
|
||||
docstuffs = python3.withPackages (ps: with ps; [ recommonmark sphinx sphinx_rtd_theme ]);
|
||||
in
|
||||
stdenv.mkDerivation {
|
||||
name = "servant-dev";
|
||||
buildInputs = [ ghc zlib python3 wget cabal-install postgresql openssl stack haskellPackages.hspec-discover ]
|
||||
++ (if tutorial then [docstuffs postgresql] else []);
|
||||
shellHook = ''
|
||||
eval $(grep export ${ghc}/bin/ghc)
|
||||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:"${zlib}/lib";
|
||||
'';
|
||||
}
|
||||
with pkgs;
|
||||
|
||||
let
|
||||
ghc = haskell.packages.${compiler}.ghcWithPackages (_: []);
|
||||
docstuffs = python3.withPackages (ps: with ps; [ recommonmark sphinx sphinx_rtd_theme ]);
|
||||
in
|
||||
|
||||
stdenv.mkDerivation {
|
||||
name = "servant-dev";
|
||||
buildInputs = [ ghc zlib python3 wget ]
|
||||
++ (if tutorial then [docstuffs postgresql] else []);
|
||||
shellHook = ''
|
||||
eval $(grep export ${ghc}/bin/ghc)
|
||||
export LD_LIBRARY_PATH="${zlib}/lib";
|
||||
'';
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
servant-auth-server/README.lhs
|
|
@ -1 +0,0 @@
|
|||
:set -isrc -itest -idoctest/ghci-wrapper/src
|
|
@ -1,26 +0,0 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||
and this project adheres to [PVP Versioning](https://pvp.haskell.org/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.4.1.0] - 2020-10-06
|
||||
|
||||
- Support generic Bearer token auth
|
||||
|
||||
## [0.4.0.0] - 2019-03-08
|
||||
|
||||
## Changed
|
||||
|
||||
- #145 Support servant-0.16 in tests @domenkozar
|
||||
- #145 Drop GHC 7.10 support @domenkozar
|
||||
|
||||
## [0.3.3.0] - 2018-06-18
|
||||
|
||||
### Added
|
||||
- Support for GHC 8.4 by @phadej
|
||||
- Support for servant-0.14 by @phadej
|
||||
- Changelog by @domenkozar
|
|
@ -1,31 +0,0 @@
|
|||
Copyright Julian K. Arni (c) 2015
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
|
||||
* Neither the name of Julian K. Arni nor the names of other
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
import Distribution.Simple
|
||||
main = defaultMain
|
|
@ -1,80 +0,0 @@
|
|||
cabal-version: 2.2
|
||||
name: servant-auth-client
|
||||
version: 0.4.1.0
|
||||
synopsis: servant-client/servant-auth compatibility
|
||||
description: This package provides instances that allow generating clients from
|
||||
<https://hackage.haskell.org/package/servant servant>
|
||||
APIs that use
|
||||
<https://hackage.haskell.org/package/servant-auth servant-auth's> @Auth@ combinator.
|
||||
.
|
||||
For a quick overview of the usage, see the <https://github.com/haskell-servant/servant/tree/master/servant-auth#readme README>.
|
||||
category: Web, Servant, Authentication
|
||||
homepage: https://github.com/haskell-servant/servant/tree/master/servant-auth#readme
|
||||
bug-reports: https://github.com/haskell-servant/servant/issues
|
||||
author: Julian K. Arni
|
||||
maintainer: jkarni@gmail.com
|
||||
copyright: (c) Julian K. Arni
|
||||
license: BSD-3-Clause
|
||||
license-file: LICENSE
|
||||
tested-with: GHC ==8.6.5 || ==8.8.4 || ==8.10.4 || ==9.0.1
|
||||
build-type: Simple
|
||||
extra-source-files:
|
||||
CHANGELOG.md
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
location: https://github.com/haskell-servant/servant
|
||||
|
||||
library
|
||||
hs-source-dirs:
|
||||
src
|
||||
default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable FlexibleContexts FlexibleInstances FunctionalDependencies GADTs KindSignatures MultiParamTypeClasses OverloadedStrings RankNTypes ScopedTypeVariables TypeFamilies TypeOperators
|
||||
ghc-options: -Wall
|
||||
build-depends:
|
||||
base >= 4.10 && < 4.18
|
||||
, bytestring >= 0.10.6.0 && < 0.12
|
||||
, containers >= 0.5.6.2 && < 0.7
|
||||
, servant-auth == 0.4.*
|
||||
, servant >= 0.13 && < 0.20
|
||||
, servant-client-core >= 0.13 && < 0.20
|
||||
|
||||
exposed-modules:
|
||||
Servant.Auth.Client
|
||||
Servant.Auth.Client.Internal
|
||||
default-language: Haskell2010
|
||||
|
||||
test-suite spec
|
||||
type: exitcode-stdio-1.0
|
||||
main-is: Spec.hs
|
||||
hs-source-dirs:
|
||||
test
|
||||
default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable FlexibleContexts FlexibleInstances FunctionalDependencies GADTs KindSignatures MultiParamTypeClasses OverloadedStrings RankNTypes ScopedTypeVariables TypeFamilies TypeOperators
|
||||
ghc-options: -Wall
|
||||
build-tool-depends: hspec-discover:hspec-discover >=2.5.5 && <2.10
|
||||
|
||||
-- dependencies with bounds inherited from the library stanza
|
||||
build-depends:
|
||||
base
|
||||
, servant-client
|
||||
, servant-auth
|
||||
, servant
|
||||
, servant-auth-client
|
||||
|
||||
-- test dependencies
|
||||
build-depends:
|
||||
hspec >= 2.5.5 && < 2.10
|
||||
, QuickCheck >= 2.11.3 && < 2.15
|
||||
, aeson >= 1.3.1.1 && < 3
|
||||
, bytestring >= 0.10.6.0 && < 0.12
|
||||
, http-client >= 0.5.13.1 && < 0.8
|
||||
, http-types >= 0.12.2 && < 0.13
|
||||
, servant-auth-server >= 0.4.2.0 && < 0.5
|
||||
, servant-server >= 0.13 && < 0.20
|
||||
, time >= 1.5.0.1 && < 1.13
|
||||
, transformers >= 0.4.2.0 && < 0.6
|
||||
, wai >= 3.2.1.2 && < 3.3
|
||||
, warp >= 3.2.25 && < 3.4
|
||||
, jose >= 0.10 && < 0.11
|
||||
other-modules:
|
||||
Servant.Auth.ClientSpec
|
||||
default-language: Haskell2010
|
|
@ -1,3 +0,0 @@
|
|||
module Servant.Auth.Client (Token(..), Bearer) where
|
||||
|
||||
import Servant.Auth.Client.Internal (Bearer, Token(..))
|
|
@ -1,64 +0,0 @@
|
|||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
#if __GLASGOW_HASKELL__ == 800
|
||||
{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
|
||||
#endif
|
||||
module Servant.Auth.Client.Internal where
|
||||
|
||||
import qualified Data.ByteString as BS
|
||||
import Data.Monoid
|
||||
import Data.Proxy (Proxy (..))
|
||||
import Data.String (IsString)
|
||||
import GHC.Exts (Constraint)
|
||||
import GHC.Generics (Generic)
|
||||
import Servant.API ((:>))
|
||||
import Servant.Auth
|
||||
|
||||
import Servant.Client.Core
|
||||
import Data.Sequence ((<|))
|
||||
|
||||
-- | A simple bearer token.
|
||||
newtype Token = Token { getToken :: BS.ByteString }
|
||||
deriving (Eq, Show, Read, Generic, IsString)
|
||||
|
||||
type family HasBearer xs :: Constraint where
|
||||
HasBearer (Bearer ': xs) = ()
|
||||
HasBearer (JWT ': xs) = ()
|
||||
HasBearer (x ': xs) = HasBearer xs
|
||||
HasBearer '[] = BearerAuthNotEnabled
|
||||
|
||||
class BearerAuthNotEnabled
|
||||
|
||||
-- | @'HasBearer' auths@ is nominally a redundant constraint, but ensures we're not
|
||||
-- trying to send a token to an API that doesn't accept them.
|
||||
instance (HasBearer auths, HasClient m api) => HasClient m (Auth auths a :> api) where
|
||||
type Client m (Auth auths a :> api) = Token -> Client m api
|
||||
|
||||
clientWithRoute m _ req (Token token)
|
||||
= clientWithRoute m (Proxy :: Proxy api)
|
||||
$ req { requestHeaders = ("Authorization", headerVal) <| requestHeaders req }
|
||||
where
|
||||
headerVal = "Bearer " <> token
|
||||
|
||||
#if MIN_VERSION_servant_client_core(0,14,0)
|
||||
hoistClientMonad pm _ nt cl = hoistClientMonad pm (Proxy :: Proxy api) nt . cl
|
||||
#endif
|
||||
|
||||
|
||||
-- * Authentication combinators
|
||||
|
||||
-- | A Bearer token in the Authorization header:
|
||||
--
|
||||
-- @Authorization: Bearer <token>@
|
||||
--
|
||||
-- This can be any token recognized by the server, for example,
|
||||
-- a JSON Web Token (JWT).
|
||||
--
|
||||
-- Note that, since the exact way the token is validated is not specified,
|
||||
-- this combinator can only be used in the client. The server would not know
|
||||
-- how to validate it, while the client does not care.
|
||||
-- If you want to implement Bearer authentication in your server, you have to
|
||||
-- choose a specific combinator, such as 'JWT'.
|
||||
data Bearer
|
|
@ -1,161 +0,0 @@
|
|||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
module Servant.Auth.ClientSpec (spec) where
|
||||
|
||||
import Crypto.JOSE (JWK,
|
||||
KeyMaterialGenParam (OctGenParam),
|
||||
genJWK)
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..))
|
||||
import qualified Data.ByteString.Lazy as BSL
|
||||
import Data.Time (UTCTime, defaultTimeLocale,
|
||||
parseTimeOrError)
|
||||
import GHC.Generics (Generic)
|
||||
import Network.HTTP.Client (Manager, defaultManagerSettings,
|
||||
newManager)
|
||||
import Network.HTTP.Types (status401)
|
||||
import Network.Wai.Handler.Warp (testWithApplication)
|
||||
import Servant
|
||||
import Servant.Client (BaseUrl (..), Scheme (Http),
|
||||
ClientError (FailureResponse),
|
||||
#if MIN_VERSION_servant_client(0,16,0)
|
||||
ResponseF(..),
|
||||
#elif MIN_VERSION_servant_client(0,13,0)
|
||||
GenResponse(..),
|
||||
#elif MIN_VERSION_servant_client(0,12,0)
|
||||
Response(..),
|
||||
#endif
|
||||
client)
|
||||
import System.IO.Unsafe (unsafePerformIO)
|
||||
import Test.Hspec
|
||||
import Test.QuickCheck
|
||||
|
||||
#if MIN_VERSION_servant_client(0,13,0)
|
||||
import Servant.Client (mkClientEnv, runClientM)
|
||||
#elif MIN_VERSION_servant_client(0,9,0)
|
||||
import Servant.Client (ClientEnv (..), runClientM)
|
||||
#else
|
||||
import Control.Monad.Trans.Except (runExceptT)
|
||||
#endif
|
||||
#if !MIN_VERSION_servant_server(0,16,0)
|
||||
#define ClientError ServantError
|
||||
#endif
|
||||
|
||||
import Servant.Auth.Client
|
||||
import Servant.Auth.Server
|
||||
import Servant.Auth.Server.SetCookieOrphan ()
|
||||
|
||||
spec :: Spec
|
||||
spec = describe "The JWT combinator" $ do
|
||||
hasClientSpec
|
||||
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- * HasClient {{{
|
||||
|
||||
hasClientSpec :: Spec
|
||||
hasClientSpec = describe "HasClient" $ around (testWithApplication $ return app) $ do
|
||||
|
||||
let mkTok :: User -> Maybe UTCTime -> IO Token
|
||||
mkTok user mexp = do
|
||||
Right tok <- makeJWT user jwtCfg mexp
|
||||
return $ Token $ BSL.toStrict tok
|
||||
|
||||
it "succeeds when the token does not have expiry" $ \port -> property $ \user -> do
|
||||
tok <- mkTok user Nothing
|
||||
v <- getIntClient tok mgr (BaseUrl Http "localhost" port "")
|
||||
v `shouldBe` Right (length $ name user)
|
||||
|
||||
it "succeeds when the token is not expired" $ \port -> property $ \user -> do
|
||||
tok <- mkTok user (Just future)
|
||||
v <- getIntClient tok mgr (BaseUrl Http "localhost" port "")
|
||||
v `shouldBe` Right (length $ name user)
|
||||
|
||||
it "fails when token is expired" $ \port -> property $ \user -> do
|
||||
tok <- mkTok user (Just past)
|
||||
#if MIN_VERSION_servant_client(0,16,0)
|
||||
Left (FailureResponse _ (Response stat _ _ _))
|
||||
#elif MIN_VERSION_servant_client(0,12,0)
|
||||
Left (FailureResponse (Response stat _ _ _))
|
||||
#elif MIN_VERSION_servant_client(0,11,0)
|
||||
Left (FailureResponse _ stat _ _)
|
||||
#else
|
||||
Left (FailureResponse stat _ _)
|
||||
#endif
|
||||
<- getIntClient tok mgr (BaseUrl Http "localhost" port "")
|
||||
stat `shouldBe` status401
|
||||
|
||||
|
||||
getIntClient :: Token -> Manager -> BaseUrl -> IO (Either ClientError Int)
|
||||
#if MIN_VERSION_servant(0,13,0)
|
||||
getIntClient tok m burl = runClientM (client api tok) (mkClientEnv m burl)
|
||||
#elif MIN_VERSION_servant(0,9,0)
|
||||
getIntClient tok m burl = runClientM (client api tok) (ClientEnv m burl)
|
||||
#else
|
||||
getIntClient tok m burl = runExceptT $ client api tok m burl
|
||||
#endif
|
||||
-- }}}
|
||||
------------------------------------------------------------------------------
|
||||
-- * API and Server {{{
|
||||
|
||||
type API = Auth '[JWT] User :> Get '[JSON] Int
|
||||
|
||||
api :: Proxy API
|
||||
api = Proxy
|
||||
|
||||
theKey :: JWK
|
||||
theKey = unsafePerformIO . genJWK $ OctGenParam 256
|
||||
{-# NOINLINE theKey #-}
|
||||
|
||||
mgr :: Manager
|
||||
mgr = unsafePerformIO $ newManager defaultManagerSettings
|
||||
{-# NOINLINE mgr #-}
|
||||
|
||||
app :: Application
|
||||
app = serveWithContext api ctx server
|
||||
where
|
||||
ctx = cookieCfg :. jwtCfg :. EmptyContext
|
||||
|
||||
jwtCfg :: JWTSettings
|
||||
jwtCfg = defaultJWTSettings theKey
|
||||
|
||||
cookieCfg :: CookieSettings
|
||||
cookieCfg = defaultCookieSettings
|
||||
|
||||
|
||||
server :: Server API
|
||||
server = getInt
|
||||
where
|
||||
getInt :: AuthResult User -> Handler Int
|
||||
getInt (Authenticated u) = return . length $ name u
|
||||
getInt _ = throwAll err401
|
||||
|
||||
|
||||
-- }}}
|
||||
------------------------------------------------------------------------------
|
||||
-- * Utils {{{
|
||||
|
||||
past :: UTCTime
|
||||
past = parseTimeOrError True defaultTimeLocale "%Y-%m-%d" "1970-01-01"
|
||||
|
||||
future :: UTCTime
|
||||
future = parseTimeOrError True defaultTimeLocale "%Y-%m-%d" "2070-01-01"
|
||||
|
||||
|
||||
-- }}}
|
||||
------------------------------------------------------------------------------
|
||||
-- * Types {{{
|
||||
|
||||
data User = User
|
||||
{ name :: String
|
||||
, _id :: String
|
||||
} deriving (Eq, Show, Read, Generic)
|
||||
|
||||
instance FromJWT User
|
||||
instance ToJWT User
|
||||
instance FromJSON User
|
||||
instance ToJSON User
|
||||
|
||||
instance Arbitrary User where
|
||||
arbitrary = User <$> arbitrary <*> arbitrary
|
||||
|
||||
-- }}}
|
|
@ -1 +0,0 @@
|
|||
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
|
|
@ -1 +0,0 @@
|
|||
:set -isrc -itest -idoctest/ghci-wrapper/src
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue