git.iko.soy/typeable/servant
on master help sign in
125 files changed+3883−459
since splitsnapshots
unifiedsplit
.github/workflows/master.yml+17−29 .gitignore+1 CONTRIBUTING.md+4−2 cabal.project+1−23 changelog.d/1469+11 changelog.d/1529+10 changelog.d/1556+81 changelog.d/1569+13 changelog.d/1580+12 changelog.d/1589+2 changelog.d/1595+2 doc/Makefile+1−1 doc/conf.py+1−2 doc/cookbook/basic-auth/basic-auth.cabal+1−1 doc/cookbook/basic-streaming/basic-streaming.cabal+1−1 doc/cookbook/curl-mock/curl-mock.cabal+4−1 doc/cookbook/custom-errors/custom-errors.cabal+1−1 doc/cookbook/db-mysql-basics/mysql-basics.cabal+1−1 doc/cookbook/db-postgres-pool/db-postgres-pool.cabal+1−1 doc/cookbook/db-sqlite-simple/db-sqlite-simple.cabal+1−1 doc/cookbook/file-upload/file-upload.cabal+1−1 doc/cookbook/generic/generic.cabal+1−1 doc/cookbook/hoist-server-with-context/hoist-server-with-context.cabal+1−1 doc/cookbook/https/https.cabal+1−1 doc/cookbook/index.rst+2−2 doc/cookbook/jwt-and-basic-auth/jwt-and-basic-auth.cabal+1−1 doc/cookbook/open-id-connect/OpenIdConnect.cabal+1−1 doc/cookbook/pagination/pagination.cabal+1−1 doc/cookbook/sentry/sentry.cabal+1−1 doc/cookbook/structuring-apis/structuring-apis.cabal+1−1 doc/cookbook/testing/testing.cabal+1−1 doc/cookbook/using-custom-monad/using-custom-monad.cabal+1−1 doc/cookbook/using-free-client/UsingFreeClient.lhs+1−1 doc/cookbook/using-free-client/using-free-client.cabal+1−1 doc/cookbook/uverb/uverb.cabal+1−1 doc/links.rst+1−1 doc/tutorial/install.rst+26 doc/tutorial/tutorial.cabal+2−2 servant-auth/servant-auth-client/servant-auth-client.cabal+13−13 servant-auth/servant-auth-docs/servant-auth-docs.cabal+9−9 servant-auth/servant-auth-server/servant-auth-server.cabal+18−18 servant-auth/servant-auth-server/src/Servant/Auth/Server/Internal/AddSetCookie.hs+12 servant-auth/servant-auth-server/src/Servant/Auth/Server/Internal/ConfigTypes.hs+2−2 servant-auth/servant-auth-server/src/Servant/Auth/Server/Internal/JWT.hs+4−3 servant-auth/servant-auth-server/src/Servant/Auth/Server/Internal/ThrowAll.hs+10−1 servant-auth/servant-auth-server/test/Servant/Auth/ServerSpec.hs+7 servant-auth/servant-auth-swagger/servant-auth-swagger.cabal+12−16 servant-auth/servant-auth/servant-auth.cabal+11−10 servant-auth/servant-auth/src/Servant/Auth/JWT.hs+9−2 servant-client-core/CHANGELOG.md+31 servant-client-core/servant-client-core.cabal+11−10 servant-client-core/src/Servant/Client/Core.hs+1 servant-client-core/src/Servant/Client/Core/HasClient.hs+170−36 servant-client-core/src/Servant/Client/Core/Reexport.hs+4 servant-client-core/src/Servant/Client/Core/Request.hs+25−8 servant-client-core/src/Servant/Client/Generic.hs+8−11 servant-client-core/test/Servant/Client/Core/RequestSpec.hs+12 servant-client-ghcjs/servant-client-ghcjs.cabal+4−4 servant-client-ghcjs/src/Servant/Client/Internal/XhrClient.hs+8−5 servant-client/CHANGELOG.md+31 servant-client/servant-client.cabal+14−12 servant-client/src/Servant/Client/Internal/HttpClient.hs+11−11 servant-client/src/Servant/Client/Internal/HttpClient/Streaming.hs+5−7 servant-client/test/Servant/BrokenSpec.hs+71 servant-client/test/Servant/ClientTestUtils.hs+48−4 servant-client/test/Servant/FailSpec.hs+3−3 servant-client/test/Servant/GenericSpec.hs+37 servant-client/test/Servant/SuccessSpec.hs+21−6 servant-conduit/servant-conduit.cabal+5−5 servant-docs/CHANGELOG.md+9 servant-docs/servant-docs.cabal+9−9 servant-docs/src/Servant/Docs/Internal.hs+4 servant-foreign/servant-foreign.cabal+8−8 servant-http-streams/CHANGELOG.md+10 servant-http-streams/servant-http-streams.cabal+14−14 servant-http-streams/src/Servant/HttpStreams/Internal.hs+6−6 servant-machines/servant-machines.cabal+5−5 servant-pipes/servant-pipes.cabal+6−6 servant-server/CHANGELOG.md+24 servant-server/example/greet.hs+16−2 servant-server/servant-server.cabal+11−10 servant-server/src/Servant/Server.hs+3−2 servant-server/src/Servant/Server/Generic.hs+11−18 servant-server/src/Servant/Server/Internal.hs+107−53 servant-server/src/Servant/Server/Internal/Handler.hs+7−2 servant-server/src/Servant/Server/Internal/Router.hs+31−14 servant-server/test/Servant/Server/RouterSpec.hs+127−6 servant-swagger/CHANGELOG.md+143 servant-swagger/LICENSE+28 servant-swagger/README.md+46 servant-swagger/Setup.hs+33 servant-swagger/example/LICENSE+28 servant-swagger/example/example.cabal+62 servant-swagger/example/server/Main.hs+11 servant-swagger/example/src/Todo.hs+73 servant-swagger/example/swagger.json+158 servant-swagger/example/test/Spec.hs+1 servant-swagger/example/test/TodoSpec.hs+28 servant-swagger/servant-swagger.cabal+127 servant-swagger/src/Servant/Swagger.hs+188 servant-swagger/src/Servant/Swagger/Internal.hs+488 servant-swagger/src/Servant/Swagger/Internal/Orphans.hs+27 servant-swagger/src/Servant/Swagger/Internal/Test.hs+207 servant-swagger/src/Servant/Swagger/Internal/TypeLevel.hs+9 servant-swagger/src/Servant/Swagger/Internal/TypeLevel/API.hs+86 servant-swagger/src/Servant/Swagger/Internal/TypeLevel/Every.hs+63 servant-swagger/src/Servant/Swagger/Internal/TypeLevel/TMap.hs+37 servant-swagger/src/Servant/Swagger/Test.hs+13 servant-swagger/src/Servant/Swagger/TypeLevel.hs+15 servant-swagger/test/Servant/SwaggerSpec.hs+489 servant-swagger/test/Spec.hs+1 servant-swagger/test/doctests.hs+12 servant/CHANGELOG.md+116 servant/servant.cabal+12−9 servant/src/Servant/API.hs+9 servant/src/Servant/API/ContentTypes.hs+1−1 servant/src/Servant/API/NamedRoutes.hs+10 servant/src/Servant/API/ResponseHeaders.hs+6−11 servant/src/Servant/API/Status.hs+6−1 servant/src/Servant/API/TypeErrors.hs+40 servant/src/Servant/API/Verbs.hs+2−1 servant/src/Servant/Links.hs+49−1 servant/src/Servant/Types/SourceT.hs+1−1 servant/test/Servant/API/ContentTypesSpec.hs+11 stack.yaml+1
.github/workflows/master.yml+17−29
@@ -13,12 +13,13 @@ jobs:
1313 strategy:
1414 matrix:
1515 os: [ubuntu-latest]
16− cabal: ["3.4"]
16+ cabal: ["3.6"]
1717 ghc:
1818 - "8.6.5"
1919 - "8.8.4"
2020 - "8.10.7"
21− - "9.0.1"
21+ - "9.0.2"
22+ - "9.2.2"
2223
2324 steps:
2425 - uses: actions/checkout@v2
@@ -47,10 +48,7 @@ jobs:
4748
4849 - name: Configure
4950 run: |
50− # Using separate store-dir because default one already has 'ghc-paths' package installed
51− # with hardcoded path to ghcup's GHC path (which it was built with). This leads to failure in
52− # doctest, as it tries to invoke that GHC, and it doesn't exist here.
53− cabal --store-dir /tmp/cabal-store install --ignore-project -j2 doctest --constraint='doctest ^>=0.18'
51+ cabal install --ignore-project -j2 doctest --constraint='doctest ^>=0.20'
5452
5553 - name: Build
5654 run: |
@@ -61,39 +59,29 @@ jobs:
6159 cabal test all
6260
6361 - name: Run doctests
64− # doctests are broken on GHC 9 due to compiler bug:
65− # https://gitlab.haskell.org/ghc/ghc/-/issues/19460
66− continue-on-error: ${{ matrix.ghc == '9.0.1' }}
6762 run: |
6863 # Necessary for doctest to be found in $PATH
6964 export PATH="$HOME/.cabal/bin:$PATH"
7065
71− # Filter out base-compat-batteries from .ghc.environment.*, as its modules
72− # conflict with those of base-compat.
73− #
74− # FIXME: This is an ugly hack. Ultimately, we'll want to use cabal-doctest
75− # (or cabal v2-doctest, if it ever lands) to provide a clean GHC environment.
76− # This might allow running doctests in GHCJS build as well.
77− perl -i -e 'while (<ARGV>) { print unless /package-id\s+(base-compat-batteries)-\d+(\.\d+)*/; }' .ghc.environment.*
78−
79− (cd servant && doctest src)
80− (cd servant-client && doctest src)
81− (cd servant-client-core && doctest src)
82− (cd servant-http-streams && doctest src)
83− (cd servant-docs && doctest src)
84− (cd servant-foreign && doctest src)
85− (cd servant-server && doctest src)
86− (cd servant-machines && doctest src)
87− (cd servant-conduit && doctest src)
88− (cd servant-pipes && doctest src)
66+ DOCTEST="cabal repl --with-ghc=doctest --ghc-options=-w"
67+ (cd servant && eval $DOCTEST)
68+ (cd servant-client && eval $DOCTEST)
69+ (cd servant-client-core && eval $DOCTEST)
70+ (cd servant-http-streams && eval $DOCTEST)
71+ (cd servant-docs && eval $DOCTEST)
72+ (cd servant-foreign && eval $DOCTEST)
73+ (cd servant-server && eval $DOCTEST)
74+ (cd servant-machines && eval $DOCTEST)
75+ (cd servant-conduit && eval $DOCTEST)
76+ (cd servant-pipes && eval $DOCTEST)
8977
9078 stack:
9179 name: stack / ghc ${{ matrix.ghc }}
9280 runs-on: ubuntu-latest
9381 strategy:
9482 matrix:
95− stack: ["2.7.3"]
96− ghc: ["8.10.4"]
83+ stack: ["2.7.5"]
84+ ghc: ["8.10.7"]
9785
9886 steps:
9987 - uses: actions/checkout@v2
.gitignore+1
@@ -31,6 +31,7 @@ doc/venv
3131doc/tutorial/static/api.js
3232doc/tutorial/static/jq.js
3333shell.nix
34+.hspec-failures
3435
3536# nix
3637result*
CONTRIBUTING.md+4−2
@@ -79,8 +79,10 @@ not been a timely response to a PR, you can ping the Maintainers group (with
7979We encourage people to experiment with new combinators and instances - it is
8080one of the most powerful ways of using `servant`, and a wonderful way of
8181getting to know it better. If you do write a new combinator, we would love to
82−know about it! Either hop on #servant on freenode and let us know, or open an
83−issue with the `news` tag (which we will close when we read it).
82+know about it! Either hop on
83+[#haskell-servant on libera.chat](https://web.libera.chat/#haskell-servant) and
84+let us know, or open an issue with the `news` tag (which we will close when we
85+read it).
8486
8587As for adding them to the main repo: maintaining combinators can be expensive,
8688since official combinators must have instances for all classes (and new classes
cabal.project+1−23
@@ -12,6 +12,7 @@ packages:
1212 servant-docs/
1313 servant-foreign/
1414 servant-server/
15+ servant-swagger/
1516 doc/tutorial/
1617
1718-- servant streaming
@@ -50,26 +51,3 @@ packages:
5051tests: True
5152optimization: False
5253-- reorder-goals: True
53−
54−constraints:
55− -- see https://github.com/haskell-infra/hackage-trustees/issues/119
56− foundation >=0.0.14,
57− memory <0.14.12 || >0.14.12
58−
59−constraints: base-compat ^>=0.11
60−constraints: semigroups ^>=0.19
61−
62−-- allow-newer: sqlite-simple-0.4.16.0:semigroups
63−-- allow-newer: direct-sqlite-2.3.24:semigroups
64−
65−-- needed for doctests
66−write-ghc-environment-files: always
67−
68−-- https://github.com/chordify/haskell-servant-pagination/pull/12
69−allow-newer: servant-pagination-2.2.2:servant
70−allow-newer: servant-pagination-2.2.2:servant-server
71−
72−allow-newer: servant-js:servant
73−
74−-- ghc 9
75−allow-newer: tdigest:base
changelog.d/1469new file+11
@@ -0,0 +1,11 @@
1+synopsis: Derive HasClient good response status from Verb status
2+prs: #1469
3+description: {
4+`HasClient` instances for the `Verb` datatype use `runRequest` in
5+`clientWithRoute` definitions.
6+This means that a request performed with `runClientM` will be successful if and
7+only if the endpoint specify a response status code >=200 and <300.
8+This change replaces `runRequest` with `runRequestAcceptStatus` in `Verb`
9+instances for the `HasClient` class, deriving the good response status from
10+the `Verb` status.
11+}
changelog.d/1529new file+10
@@ -0,0 +1,10 @@
1+synopsis: Fix performRequest in servant-client-ghcjs
2+prs: #1529
3+
4+description: {
5+
6+performRequest function in servant-client-ghcjs was not compatible with the
7+latest RunClient typeclass. Added the acceptStatus parameter and fixed the
8+functionality to match what servant-client provides.
9+
10+}
changelog.d/1556new file+81
@@ -0,0 +1,81 @@
1+synopsis: Display capture hints in router layout
2+prs: #1556
3+
4+description: {
5+
6+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.
7+
8+Example:
9+
10+For the following API
11+```haskell
12+type API =
13+ "a" :> "d" :> Get '[JSON] NoContent
14+ :<|> "b" :> Capture "x" Int :> Get '[JSON] Bool
15+ :<|> "a" :> "e" :> Get '[JSON] Int
16+```
17+
18+we previously got the following output:
19+
20+```
21+/
22+├─ a/
23+│ ├─ d/
24+│ │ └─•
25+│ └─ e/
26+│ └─•
27+└─ b/
28+ └─ <capture>/
29+ ├─•
30+ ┆
31+ └─•
32+```
33+
34+now we get:
35+
36+```
37+/
38+├─ a/
39+│ ├─ d/
40+│ │ └─•
41+│ └─ e/
42+│ └─•
43+└─ b/
44+ └─ <x::Int>/
45+ ├─•
46+ ┆
47+ └─•
48+```
49+
50+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.
51+CaptureHint values are then used in routerLayout, to display the name and type of captured values, instead of just `<capture>` previously.
52+
53+N.B.:
54+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.
55+
56+This PR also introduces Spec tests for the routerLayout function.
57+
58+Warning:
59+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`.
60+In those cases, the fix is easy: it suffices to add `Typeable a` to the left-hand side of the quantification constraint.
61+
62+For instance, the following code will no longer compile:
63+```haskell
64+type MyAPI a = Capture "foo" a :> Get '[JSON] ()
65+
66+myServer :: forall a. Server (MyAPI a)
67+myServer = const $ return ()
68+
69+myApi :: forall a. Proxy (MyAPI a)
70+myApi = Proxy
71+
72+app :: forall a. (FromHttpApiData a) => Application
73+app = serve (myApi @a) (myServer @a)
74+```
75+
76+Indeed, `app` should be replaced with:
77+```haskell
78+app :: forall a. (FromHttpApiData a, Typeable a) => Application
79+app = serve (myApi @a) (myServer @a)
80+```
81+}
changelog.d/1569new file+13
@@ -0,0 +1,13 @@
1+synopsis: Encode captures using toEncodedUrlPiece
2+prs: #1569
3+issues: #1511
4+
5+description: {
6+The `servant-client` library now makes direct use of `toEncodedUrlPiece` from `ToHttpApiData`
7+to encode captured values when building the request path. It gives user freedom to implement
8+URL-encoding however they need.
9+
10+Previous behavior was to use `toUrlPiece` and URL-encode its output using `toEncodedUrlPiece`
11+from the `Text` instance of `ToHttpApiData`. The issue with this approach is that
12+`ToHttpApiData Text` is overly zealous and also encodes characters, such as `*`, which are perfectly valid in a URL.
13+}
changelog.d/1580new file+12
@@ -0,0 +1,12 @@
1+synopsis: Allow IO in validationKeys
2+prs: #1580
3+issues: #1579
4+
5+description: {
6+
7+Currently validationKeys are a fixed JWKSet. This does not work with OIDC
8+providers such as AWS Cognito or Okta, which regularly fetching jwks_uri to
9+discover new and expired keys.
10+
11+This change alters the type of validationKeys from JWKSet to IO JWKSet.
12+}
changelog.d/1589new file+2
@@ -0,0 +1,2 @@
1+synopsis: Only include question mark for nonempty query strings
2+prs: 1589
changelog.d/1595new file+2
@@ -0,0 +1,2 @@
1+synopsis: Run ClientEnv's makeClientRequest in IO.
2+prs: #1595
doc/Makefile+1−1
@@ -10,7 +10,7 @@ BUILDDIR = _build
1010
1111# Put it first so that "make" without argument is like "make help".
1212help:
13− @if [ ! -d venv ]; then echo "WARNING: There is no venv directory, did you forget to 'virtualenv venv'. Check building-the-docs file."; fi
13+ @if [ ! -d venv ]; then echo "WARNING: There is no venv directory, did you forget to 'virtualenv venv'. Check README.md."; fi
1414 @if [ ! "z$$(which $(SPHINXBUILD))" = "z$$(pwd)/venv/bin/sphinx-build" ]; then echo "WARNING: Did you forgot to 'source venv/bin/activate'"; fi
1515 @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
1616
doc/conf.py+1−2
@@ -46,7 +46,7 @@ master_doc = 'index'
4646
4747# General information about the project.
4848project = u'Servant'
49−copyright = u'2018, Servant Contributors'
49+copyright = u'2022, Servant Contributors'
5050author = u'Servant Contributors'
5151
5252# The version info for the project you're documenting, acts as replacement for
@@ -169,4 +169,3 @@ texinfo_documents = [
169169source_parsers = {
170170 '.lhs': CommonMarkParser,
171171}
172−
doc/cookbook/basic-auth/basic-auth.cabal+1−1
@@ -1,3 +1,4 @@
1+cabal-version: 2.2
12name: cookbook-basic-auth
23version: 0.1
34synopsis: Basic Authentication cookbook example
@@ -7,7 +8,6 @@ license-file: ../../../servant/LICENSE
78author: Servant Contributors
89maintainer: haskell-servant-maintainers@googlegroups.com
910build-type: Simple
10−cabal-version: >=1.10
1111tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
1212
1313executable cookbook-basic-auth
doc/cookbook/basic-streaming/basic-streaming.cabal+1−1
@@ -1,3 +1,4 @@
1+cabal-version: 2.2
12name: cookbook-basic-streaming
23version: 2.1
34synopsis: Streaming in servant without streaming libs
@@ -7,7 +8,6 @@ license-file: ../../../servant/LICENSE
78author: Servant Contributors
89maintainer: haskell-servant-maintainers@googlegroups.com
910build-type: Simple
10−cabal-version: >=1.10
1111tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
1212
1313executable cookbook-basic-streaming
doc/cookbook/curl-mock/curl-mock.cabal+4−1
@@ -1,3 +1,4 @@
1+cabal-version: 2.2
12name: cookbook-curl-mock
23version: 0.1
34synopsis: Generate curl mock requests cookbook example
@@ -7,10 +8,12 @@ license-file: ../../../servant/LICENSE
78author: Servant Contributors
89maintainer: haskell-servant-maintainers@googlegroups.com
910build-type: Simple
10−cabal-version: >=1.10
1111tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
1212
1313executable cookbock-curl-mock
14+ if impl(ghc >= 9.2)
15+ -- generic-arbitrary is incompatible
16+ buildable: False
1417 main-is: CurlMock.lhs
1518 build-depends: base == 4.*
1619 , aeson
doc/cookbook/custom-errors/custom-errors.cabal+1−1
@@ -1,3 +1,4 @@
1+cabal-version: 2.2
12name: cookbook-custom-errors
23version: 0.1
34synopsis: Return custom error messages from combinators
@@ -7,7 +8,6 @@ license-file: ../../../servant/LICENSE
78author: Servant Contributors
89maintainer: haskell-servant-maintainers@googlegroups.com
910build-type: Simple
10−cabal-version: >=1.10
1111tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
1212
1313executable cookbook-custom-errors
doc/cookbook/db-mysql-basics/mysql-basics.cabal+1−1
@@ -1,3 +1,4 @@
1+cabal-version: 2.2
12name: mysql-basics
23version: 0.1.0.0
34synopsis: Simple MySQL API cookbook example
@@ -7,7 +8,6 @@ license-file: ../../../servant/LICENSE
78author: Servant Contributors
89maintainer: haskell-servant-maintainers@googlegroups.com
910build-type: Simple
10−cabal-version: >=1.10
1111
1212executable run
1313 hs-source-dirs: .
doc/cookbook/db-postgres-pool/db-postgres-pool.cabal+1−1
@@ -1,3 +1,4 @@
1+cabal-version: 2.2
12name: cookbook-db-postgres-pool
23version: 0.1
34synopsis: Simple PostgreSQL connection pool cookbook example
@@ -7,7 +8,6 @@ license-file: ../../../servant/LICENSE
78author: Servant Contributors
89maintainer: haskell-servant-maintainers@googlegroups.com
910build-type: Simple
10−cabal-version: >=1.10
1111tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
1212
1313executable cookbook-db-postgres-pool
doc/cookbook/db-sqlite-simple/db-sqlite-simple.cabal+1−1
@@ -1,3 +1,4 @@
1+cabal-version: 2.2
12name: cookbook-db-sqlite-simple
23version: 0.1
34synopsis: Simple SQLite DB cookbook example
@@ -7,7 +8,6 @@ license-file: ../../../servant/LICENSE
78author: Servant Contributors
89maintainer: haskell-servant-maintainers@googlegroups.com
910build-type: Simple
10−cabal-version: >=1.10
1111tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
1212
1313executable cookbook-db-sqlite-simple
doc/cookbook/file-upload/file-upload.cabal+1−1
@@ -1,3 +1,4 @@
1+cabal-version: 2.2
12name: cookbook-file-upload
23version: 0.1
34synopsis: File upload cookbook example
@@ -7,7 +8,6 @@ license-file: ../../../servant/LICENSE
78author: Servant Contributors
89maintainer: haskell-servant-maintainers@googlegroups.com
910build-type: Simple
10−cabal-version: >=1.10
1111tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
1212
1313executable cookbook-file-upload
doc/cookbook/generic/generic.cabal+1−1
@@ -1,3 +1,4 @@
1+cabal-version: 2.2
12name: cookbook-generic
23version: 0.1
34synopsis: Using custom monad to pass a state between handlers
@@ -7,7 +8,6 @@ license-file: ../../../servant/LICENSE
78author: Servant Contributors
89maintainer: haskell-servant-maintainers@googlegroups.com
910build-type: Simple
10−cabal-version: >=1.10
1111tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
1212
1313executable cookbook-using-custom-monad
doc/cookbook/hoist-server-with-context/hoist-server-with-context.cabal+1−1
@@ -1,3 +1,4 @@
1+cabal-version: 2.2
12name: cookbook-hoist-server-with-context
23version: 0.0.1
34synopsis: JWT and basic access authentication with a Custom Monad cookbook example
@@ -10,7 +11,6 @@ author: Servant Contributors
1011maintainer: haskell-servant-maintainers@googlegroups.com
1112category: Servant
1213build-type: Simple
13−cabal-version: >=1.10
1414tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
1515
1616executable cookbook-hoist-server-with-context
doc/cookbook/https/https.cabal+1−1
@@ -1,3 +1,4 @@
1+cabal-version: 2.2
12name: cookbook-https
23version: 0.1
34synopsis: HTTPS cookbook example
@@ -7,7 +8,6 @@ license-file: ../../../servant/LICENSE
78author: Servant Contributors
89maintainer: haskell-servant-maintainers@googlegroups.com
910build-type: Simple
10−cabal-version: >=1.10
1111tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
1212
1313executable cookbook-https
doc/cookbook/index.rst+2−2
@@ -6,8 +6,8 @@ how to solve many common problems with servant. If you're
66interested in contributing examples of your own, feel free
77to open an issue or a pull request on
88`our github repository <https://github.com/haskell-servant/servant>`_
9−or even to just get in touch with us on the **#servant** IRC channel
10−on freenode or on
9+or even to just get in touch with us on the `**#haskell-servant** IRC channel
10+on libera.chat <https://web.libera.chat/#haskell-servant>_ or on
1111`the mailing list <https://groups.google.com/forum/#!forum/haskell-servant>`_.
1212
1313The scope is very wide. Simple and fancy authentication schemes,
doc/cookbook/jwt-and-basic-auth/jwt-and-basic-auth.cabal+1−1
@@ -1,3 +1,4 @@
1+cabal-version: 2.2
12name: cookbook-jwt-and-basic-auth
23version: 0.0.1
34synopsis: JWT and basic access authentication cookbook example
@@ -10,7 +11,6 @@ author: Servant Contributors
1011maintainer: haskell-servant-maintainers@googlegroups.com
1112category: Servant
1213build-type: Simple
13−cabal-version: >=1.10
1414tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
1515
1616executable cookbook-jwt-and-basic-auth
doc/cookbook/open-id-connect/OpenIdConnect.cabal+1−1
@@ -1,3 +1,4 @@
1+cabal-version: 2.2
12name: open-id-connect
23version: 0.1
34synopsis: OpenId Connect with Servant example
@@ -7,7 +8,6 @@ license-file: ../../../servant/LICENSE
78author: Servant Contributors
89maintainer: haskell-servant-maintainers@googlegroups.com
910build-type: Simple
10−cabal-version: >= 1.10
1111tested-with: GHC==8.6.5
1212
1313executable cookbook-openidconnect
doc/cookbook/pagination/pagination.cabal+1−1
@@ -1,3 +1,4 @@
1+cabal-version: 2.2
12name: cookbook-pagination
23version: 2.1
34synopsis: Pagination with Servant example
@@ -7,7 +8,6 @@ license-file: ../../../servant/LICENSE
78author: Servant Contributors
89maintainer: haskell-servant-maintainers@googlegroups.com
910build-type: Simple
10−cabal-version: >=1.10
1111tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
1212
1313executable cookbook-pagination
doc/cookbook/sentry/sentry.cabal+1−1
@@ -1,3 +1,4 @@
1+cabal-version: 2.2
12name: cookbook-sentry
23version: 0.1
34synopsis: Collecting runtime exceptions using Sentry
@@ -7,7 +8,6 @@ license-file: ../../../servant/LICENSE
78author: Servant Contributors
89maintainer: haskell-servant-maintainers@googlegroups.com
910build-type: Simple
10−cabal-version: >=1.10
1111tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
1212
1313executable cookbook-sentry
doc/cookbook/structuring-apis/structuring-apis.cabal+1−1
@@ -1,3 +1,4 @@
1+cabal-version: 2.2
12name: cookbook-structuring-apis
23version: 0.1
34synopsis: Example that shows how APIs can be structured
@@ -7,7 +8,6 @@ license-file: ../../../servant/LICENSE
78author: Servant Contributors
89maintainer: haskell-servant-maintainers@googlegroups.com
910build-type: Simple
10−cabal-version: >=1.10
1111tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
1212
1313executable cookbook-structuring-apis
doc/cookbook/testing/testing.cabal+1−1
@@ -1,3 +1,4 @@
1+cabal-version: 2.2
12name: cookbook-testing
23version: 0.0.1
34synopsis: Common testing patterns in Servant apps
@@ -9,7 +10,6 @@ author: Servant Contributors
910maintainer: haskell-servant-maintainers@googlegroups.com
1011category: Servant
1112build-type: Simple
12−cabal-version: >=1.10
1313tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
1414
1515executable cookbook-testing
doc/cookbook/using-custom-monad/using-custom-monad.cabal+1−1
@@ -1,3 +1,4 @@
1+cabal-version: 2.2
12name: cookbook-using-custom-monad
23version: 0.1
34synopsis: Using custom monad to pass a state between handlers
@@ -7,7 +8,6 @@ license-file: ../../../servant/LICENSE
78author: Servant Contributors
89maintainer: haskell-servant-maintainers@googlegroups.com
910build-type: Simple
10−cabal-version: >=1.10
1111tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
1212
1313executable cookbook-using-custom-monad
doc/cookbook/using-free-client/UsingFreeClient.lhs+1−1
@@ -119,7 +119,7 @@ Now we can use `servant-client`'s internals to convert servant's `Request`
119119to http-client's `Request`, and we can inspect it:
120120
121121```haskell
122− let req' = I.defaultMakeClientRequest burl req
122+ req' <- I.defaultMakeClientRequest burl req
123123 putStrLn $ "Making request: " ++ show req'
124124```
125125
doc/cookbook/using-free-client/using-free-client.cabal+1−1
@@ -1,3 +1,4 @@
1+cabal-version: 2.2
12name: cookbook-using-free-client
23version: 0.1
34synopsis: Using Free client
@@ -7,7 +8,6 @@ license-file: ../../../servant/LICENSE
78author: Servant Contributors
89maintainer: haskell-servant-maintainers@googlegroups.com
910build-type: Simple
10−cabal-version: >=1.10
1111tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
1212
1313executable cookbook-using-free-client
doc/cookbook/uverb/uverb.cabal+1−1
@@ -1,3 +1,4 @@
1+cabal-version: 2.2
12name: cookbook-uverb
23version: 0.0.1
34synopsis: How to use the 'UVerb' type.
@@ -9,7 +10,6 @@ author: Servant Contributors
910maintainer: haskell-servant-maintainers@googlegroups.com
1011category: Servant
1112build-type: Simple
12−cabal-version: >=1.10
1313tested-with: GHC==8.6.5, GHC==8.8.4, GHC==8.10.7
1414
1515executable cookbook-uverb
doc/links.rst+1−1
@@ -12,7 +12,7 @@ Helpful Links
1212 `https://github.com/haskell-servant/servant/issues <https://github.com/haskell-servant/servant/issues>`_
1313
1414- the irc channel:
15− ``#servant`` on freenode
15+ `#haskell-servant on libera.chat <https://web.libera.chat/#haskell-servant>`_
1616
1717- the mailing list:
1818 `groups.google.com/forum/#!forum/haskell-servant <https://groups.google.com/forum/#!forum/haskell-servant>`_
doc/tutorial/install.rst+26
@@ -40,3 +40,29 @@ nix
4040`Nix <https://nixos.org/nix/>`_ users should feel free to take a look at
4141the `nix/shell.nix` file in the repository and use it to provision a suitable
4242environment to build and run the examples.
43+
44+Note for Ubuntu users
45+--------
46+
47+Ubuntu's packages for `ghc`, `cabal`, and `stack` are years out of date.
48+If the instructions above fail for you,
49+try replacing the Ubuntu packages with up-to-date versions.
50+First remove the installed versions:
51+
52+.. code-block:: bash
53+
54+ # remove the obsolete versions
55+ $ sudo apt remove ghc haskell-stack cabal-install
56+
57+Then install fresh versions of the Haskell toolchain
58+using the `ghcup <https://www.haskell.org/ghcup/install/>`_ installer.
59+
60+As of February 2022, one easy way to do this is by running a bootstrap script:
61+
62+.. code-block:: bash
63+
64+ $ curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
65+
66+The script is interactive and will prompt you for details about what
67+you want installed and where. To install manually,
68+see `the detailed instructions <https://www.haskell.org/ghcup/install/#manual-install>`_.
doc/tutorial/tutorial.cabal+2−2
@@ -1,3 +1,4 @@
1+cabal-version: 2.2
12name: tutorial
23version: 0.10
34synopsis: The servant tutorial
@@ -11,7 +12,6 @@ license-file: LICENSE
1112author: Servant Contributors
1213maintainer: haskell-servant-maintainers@googlegroups.com
1314build-type: Simple
14−cabal-version: >=1.10
1515tested-with:
1616 GHC==8.6.5
1717 GHC==8.8.3, GHC ==8.10.7
@@ -64,7 +64,7 @@ library
6464 , blaze-markup >= 0.8.0.0 && < 0.9
6565 , cookie >= 0.4.3 && < 0.5
6666 , js-jquery >= 3.3.1 && < 3.4
67− , lucid >= 2.9.11 && < 2.10
67+ , lucid >= 2.9.11 && < 2.12
6868 , random >= 1.1 && < 1.3
6969 , servant-js >= 0.9 && < 0.10
7070 , time >= 1.6.0.1 && < 1.13
servant-auth/servant-auth-client/servant-auth-client.cabal+13−13
@@ -1,3 +1,4 @@
1+cabal-version: 2.2
12name: servant-auth-client
23version: 0.4.1.0
34synopsis: servant-client/servant-auth compatibility
@@ -6,9 +7,9 @@ description: This package provides instances that allow generating clients fr
67 APIs that use
78 <https://hackage.haskell.org/package/servant-auth servant-auth's> @Auth@ combinator.
89 .
9− For a quick overview of the usage, see the <http://github.com/haskell-servant/servant/servant-auth#readme README>.
10+ For a quick overview of the usage, see the <https://github.com/haskell-servant/servant/tree/master/servant-auth#readme README>.
1011category: Web, Servant, Authentication
11−homepage: http://github.com/haskell-servant/servant/servant-auth#readme
12+homepage: https://github.com/haskell-servant/servant/tree/master/servant-auth#readme
1213bug-reports: https://github.com/haskell-servant/servant/issues
1314author: Julian K. Arni
1415maintainer: jkarni@gmail.com
@@ -17,7 +18,6 @@ license: BSD-3-Clause
1718license-file: LICENSE
1819tested-with: GHC ==8.6.5 || ==8.8.4 || ==8.10.4 || ==9.0.1
1920build-type: Simple
20−cabal-version: >= 1.10
2121extra-source-files:
2222 CHANGELOG.md
2323
@@ -31,12 +31,12 @@ library
3131 default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable FlexibleContexts FlexibleInstances FunctionalDependencies GADTs KindSignatures MultiParamTypeClasses OverloadedStrings RankNTypes ScopedTypeVariables TypeFamilies TypeOperators
3232 ghc-options: -Wall
3333 build-depends:
34− base >= 4.10 && < 4.16
35− , bytestring >= 0.10.6.0 && < 0.11
34+ base >= 4.10 && < 4.17
35+ , bytestring >= 0.10.6.0 && < 0.12
3636 , containers >= 0.5.6.2 && < 0.7
3737 , servant-auth == 0.4.*
38− , servant >= 0.13 && < 0.19
39− , servant-client-core >= 0.13 && < 0.19
38+ , servant >= 0.13 && < 0.20
39+ , servant-client-core >= 0.13 && < 0.20
4040
4141 exposed-modules:
4242 Servant.Auth.Client
@@ -50,7 +50,7 @@ test-suite spec
5050 test
5151 default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable FlexibleContexts FlexibleInstances FunctionalDependencies GADTs KindSignatures MultiParamTypeClasses OverloadedStrings RankNTypes ScopedTypeVariables TypeFamilies TypeOperators
5252 ghc-options: -Wall
53− build-tool-depends: hspec-discover:hspec-discover >=2.5.5 && <2.9
53+ build-tool-depends: hspec-discover:hspec-discover >=2.5.5 && <2.10
5454
5555 -- dependencies with bounds inherited from the library stanza
5656 build-depends:
@@ -62,19 +62,19 @@ test-suite spec
6262
6363 -- test dependencies
6464 build-depends:
65− hspec >= 2.5.5 && < 2.9
65+ hspec >= 2.5.5 && < 2.10
6666 , QuickCheck >= 2.11.3 && < 2.15
67− , aeson >= 1.3.1.1 && < 1.6
68− , bytestring >= 0.10.6.0 && < 0.11
67+ , aeson >= 1.3.1.1 && < 3
68+ , bytestring >= 0.10.6.0 && < 0.12
6969 , http-client >= 0.5.13.1 && < 0.8
7070 , http-types >= 0.12.2 && < 0.13
7171 , servant-auth-server >= 0.4.2.0 && < 0.5
72− , servant-server >= 0.13 && < 0.19
72+ , servant-server >= 0.13 && < 0.20
7373 , time >= 1.5.0.1 && < 1.13
7474 , transformers >= 0.4.2.0 && < 0.6
7575 , wai >= 3.2.1.2 && < 3.3
7676 , warp >= 3.2.25 && < 3.4
77− , jose >= 0.7.0.0 && < 0.9
77+ , jose >= 0.7.0.0 && < 0.10
7878 other-modules:
7979 Servant.Auth.ClientSpec
8080 default-language: Haskell2010
servant-auth/servant-auth-docs/servant-auth-docs.cabal+9−9
@@ -1,3 +1,4 @@
1+cabal-version: 2.2
12name: servant-auth-docs
23version: 0.2.10.0
34synopsis: servant-docs/servant-auth compatibility
@@ -6,9 +7,9 @@ description: This package provides instances that allow generating docs from
67 APIs that use
78 <https://hackage.haskell.org/package/servant-auth servant-auth's> @Auth@ combinator.
89 .
9− For a quick overview of the usage, see the <http://github.com/haskell-servant/servant/servant-auth#readme README>.
10+ For a quick overview of the usage, see the <https://github.com/haskell-servant/servant/tree/master/servant-auth#readme README>.
1011category: Web, Servant, Authentication
11−homepage: http://github.com/haskell-servant/servant/servant-auth#readme
12+homepage: https://github.com/haskell-servant/servant/tree/master/servant-auth#readme
1213bug-reports: https://github.com/haskell-servant/servant/issues
1314author: Julian K. Arni
1415maintainer: jkarni@gmail.com
@@ -17,7 +18,6 @@ license: BSD-3-Clause
1718license-file: LICENSE
1819tested-with: GHC ==8.6.5 || ==8.8.4 || ==8.10.4 || ==9.0.1
1920build-type: Custom
20−cabal-version: >= 1.10
2121extra-source-files:
2222 CHANGELOG.md
2323
@@ -35,11 +35,11 @@ library
3535 default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable FlexibleContexts FlexibleInstances FunctionalDependencies GADTs KindSignatures MultiParamTypeClasses OverloadedStrings RankNTypes ScopedTypeVariables TypeFamilies TypeOperators
3636 ghc-options: -Wall
3737 build-depends:
38− base >= 4.10 && < 4.16
39− , servant-docs >= 0.11.2 && < 0.12
40− , servant >= 0.13 && < 0.19
38+ base >= 4.10 && < 4.17
39+ , servant-docs >= 0.11.2 && < 0.13
40+ , servant >= 0.13 && < 0.20
4141 , servant-auth == 0.4.*
42− , lens >= 4.16.1 && <5.1
42+ , lens >= 4.16.1 && <5.2
4343 exposed-modules:
4444 Servant.Auth.Docs
4545 default-language: Haskell2010
@@ -64,7 +64,7 @@ test-suite spec
6464 test
6565 default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable FlexibleContexts FlexibleInstances FunctionalDependencies GADTs KindSignatures MultiParamTypeClasses OverloadedStrings RankNTypes ScopedTypeVariables TypeFamilies TypeOperators
6666 ghc-options: -Wall
67− build-tool-depends: hspec-discover:hspec-discover >=2.5.5 && <2.9
67+ build-tool-depends: hspec-discover:hspec-discover >=2.5.5 && <2.10
6868
6969 -- dependencies with bounds inherited from the library stanza
7070 build-depends:
@@ -78,7 +78,7 @@ test-suite spec
7878 -- test dependencies
7979 build-depends:
8080 servant-auth-docs
81− , hspec >= 2.5.5 && < 2.9
81+ , hspec >= 2.5.5 && < 2.10
8282 , QuickCheck >= 2.11.3 && < 2.15
8383
8484 default-language: Haskell2010
servant-auth/servant-auth-server/servant-auth-server.cabal+18−18
@@ -1,14 +1,15 @@
1+cabal-version: 2.2
12name: servant-auth-server
2−version: 0.4.6.0
3+version: 0.4.7.0
34synopsis: servant-server/servant-auth compatibility
45description: This package provides the required instances for using the @Auth@ combinator
56 in your 'servant' server.
67 .
78 Both cookie- and token- (REST API) based authentication is provided.
89 .
9− For a quick overview of the usage, see the <http://github.com/haskell-servant/servant/servant-auth#readme README>.
10+ For a quick overview of the usage, see the <https://github.com/haskell-servant/servant/tree/master/servant-auth#readme README>.
1011category: Web, Servant, Authentication
11−homepage: http://github.com/haskell-servant/servant/servant-auth#readme
12+homepage: https://github.com/haskell-servant/servant/tree/master/servant-auth#readme
1213bug-reports: https://github.com/haskell-servant/servant/issues
1314author: Julian K. Arni
1415maintainer: jkarni@gmail.com
@@ -17,7 +18,6 @@ license: BSD-3-Clause
1718license-file: LICENSE
1819tested-with: GHC ==8.6.5 || ==8.8.4 || ==8.10.4 || ==9.0.1
1920build-type: Simple
20−cabal-version: >= 1.10
2121extra-source-files:
2222 CHANGELOG.md
2323
@@ -31,27 +31,27 @@ library
3131 default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable FlexibleContexts FlexibleInstances FunctionalDependencies GADTs KindSignatures MultiParamTypeClasses OverloadedStrings RankNTypes ScopedTypeVariables TypeFamilies TypeOperators
3232 ghc-options: -Wall
3333 build-depends:
34− base >= 4.10 && < 4.16
35− , aeson >= 1.3.1.1 && < 1.6
36− , base64-bytestring >= 1.0.0.1 && < 1.3
34+ base >= 4.10 && < 4.17
35+ , aeson >= 1.0.0.1 && < 3
36+ , base64-bytestring >= 1.0.0.1 && < 2
3737 , blaze-builder >= 0.4.1.0 && < 0.5
38− , bytestring >= 0.10.6.0 && < 0.11
38+ , bytestring >= 0.10.6.0 && < 0.12
3939 , case-insensitive >= 1.2.0.11 && < 1.3
4040 , cookie >= 0.4.4 && < 0.5
4141 , data-default-class >= 0.1.2.0 && < 0.2
4242 , entropy >= 0.4.1.3 && < 0.5
4343 , http-types >= 0.12.2 && < 0.13
44− , jose >= 0.7.0.0 && < 0.9
45− , lens >= 4.16.1 && < 5.1
46− , memory >= 0.14.16 && < 0.17
44+ , jose >= 0.7.0.0 && < 0.10
45+ , lens >= 4.16.1 && < 5.2
46+ , memory >= 0.14.16 && < 0.18
4747 , monad-time >= 0.3.1.0 && < 0.4
4848 , mtl >= 2.2.2 && < 2.3
49− , servant >= 0.13 && < 0.19
49+ , servant >= 0.13 && < 0.20
5050 , servant-auth == 0.4.*
51− , servant-server >= 0.13 && < 0.19
51+ , servant-server >= 0.13 && < 0.20
5252 , tagged >= 0.8.4 && < 0.9
53− , text >= 1.2.3.0 && < 1.3
54− , time >= 1.5.0.1 && < 1.10
53+ , text >= 1.2.3.0 && < 2.1
54+ , time >= 1.5.0.1 && < 1.12
5555 , unordered-containers >= 0.2.9.0 && < 0.3
5656 , wai >= 3.2.1.2 && < 3.3
5757
@@ -102,7 +102,7 @@ test-suite spec
102102 test
103103 default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable FlexibleContexts FlexibleInstances FunctionalDependencies GADTs KindSignatures MultiParamTypeClasses OverloadedStrings RankNTypes ScopedTypeVariables TypeFamilies TypeOperators
104104 ghc-options: -Wall
105− build-tool-depends: hspec-discover:hspec-discover >=2.5.5 && <2.8
105+ build-tool-depends: hspec-discover:hspec-discover >=2.5.5 && <2.10
106106
107107 -- dependencies with bounds inherited from the library stanza
108108 build-depends:
@@ -123,10 +123,10 @@ test-suite spec
123123 -- test dependencies
124124 build-depends:
125125 servant-auth-server
126− , hspec >= 2.5.5 && < 2.8
126+ , hspec >= 2.5.5 && < 2.10
127127 , QuickCheck >= 2.11.3 && < 2.15
128128 , http-client >= 0.5.13.1 && < 0.8
129− , lens-aeson >= 1.0.2 && < 1.2
129+ , lens-aeson >= 1.0.2 && < 1.3
130130 , warp >= 3.2.25 && < 3.4
131131 , wreq >= 0.5.2.1 && < 0.6
132132 other-modules:
servant-auth/servant-auth-server/src/Servant/Auth/Server/Internal/AddSetCookie.hs+12
@@ -11,6 +11,8 @@ import Data.Tagged (Tagged (..))
1111import qualified Network.HTTP.Types as HTTP
1212import Network.Wai (mapResponseHeaders)
1313import Servant
14+import Servant.API.Generic
15+import Servant.Server.Generic
1416import Web.Cookie
1517
1618-- What are we doing here? Well, the idea is to add headers to the response,
@@ -34,6 +36,7 @@ type family AddSetCookieApiVerb a where
3436type family AddSetCookieApi a :: *
3537type instance AddSetCookieApi (a :> b) = a :> AddSetCookieApi b
3638type instance AddSetCookieApi (a :<|> b) = AddSetCookieApi a :<|> AddSetCookieApi b
39+type instance AddSetCookieApi (NamedRoutes api) = AddSetCookieApi (ToServantApi api)
3740type instance AddSetCookieApi (Verb method stat ctyps a)
3841 = Verb method stat ctyps (AddSetCookieApiVerb a)
3942type instance AddSetCookieApi Raw = Raw
@@ -72,6 +75,15 @@ instance {-# OVERLAPS #-}
7275 => AddSetCookies ('S n) (a :<|> b) (a' :<|> b') where
7376 addSetCookies cookies (a :<|> b) = addSetCookies cookies a :<|> addSetCookies cookies b
7477
78+instance {-# OVERLAPS #-}
79+ ( AddSetCookies ('S n) (ServerT (ToServantApi api) m) cookiedApi
80+ , Generic (api (AsServerT m))
81+ , GServantProduct (Rep (api (AsServerT m)))
82+ , ToServant api (AsServerT m) ~ ServerT (ToServantApi api) m
83+ )
84+ => AddSetCookies ('S n) (api (AsServerT m)) cookiedApi where
85+ addSetCookies cookies = addSetCookies cookies . toServant
86+
7587-- | for @servant <0.11@
7688instance
7789 AddSetCookies ('S n) Application Application where
servant-auth/servant-auth-server/src/Servant/Auth/Server/Internal/ConfigTypes.hs+2−2
@@ -33,7 +33,7 @@ data JWTSettings = JWTSettings
3333 -- | Algorithm used to sign JWT.
3434 , jwtAlg :: Maybe Jose.Alg
3535 -- | Keys used to validate JWT.
36− , validationKeys :: Jose.JWKSet
36+ , validationKeys :: IO Jose.JWKSet
3737 -- | An @aud@ predicate. The @aud@ is a string or URI that identifies the
3838 -- intended recipient of the JWT.
3939 , audienceMatches :: Jose.StringOrURI -> IsMatch
@@ -44,7 +44,7 @@ defaultJWTSettings :: Jose.JWK -> JWTSettings
4444defaultJWTSettings k = JWTSettings
4545 { signingKey = k
4646 , jwtAlg = Nothing
47− , validationKeys = Jose.JWKSet [k]
47+ , validationKeys = pure $ Jose.JWKSet [k]
4848 , audienceMatches = const Matches }
4949
5050-- | The policies to use when generating cookies.
servant-auth/servant-auth-server/src/Servant/Auth/Server/Internal/JWT.hs+4−3
@@ -58,14 +58,15 @@ makeJWT v cfg expiry = runExceptT $ do
5858
5959verifyJWT :: FromJWT a => JWTSettings -> BS.ByteString -> IO (Maybe a)
6060verifyJWT jwtCfg input = do
61− verifiedJWT <- liftIO $ runExceptT $ do
61+ keys <- validationKeys jwtCfg
62+ verifiedJWT <- runExceptT $ do
6263 unverifiedJWT <- Jose.decodeCompact (BSL.fromStrict input)
6364 Jose.verifyClaims
6465 (jwtSettingsToJwtValidationSettings jwtCfg)
65− (validationKeys jwtCfg)
66+ keys
6667 unverifiedJWT
6768 return $ case verifiedJWT of
6869 Left (_ :: Jose.JWTError) -> Nothing
6970 Right v -> case decodeJWT v of
7071 Left _ -> Nothing
71− Right v' -> Just v'
72+ Right v' -> Just v'
servant-auth/servant-auth-server/src/Servant/Auth/Server/Internal/ThrowAll.hs+10−1
@@ -8,7 +8,10 @@ module Servant.Auth.Server.Internal.ThrowAll where
88
99import Control.Monad.Error.Class
1010import Data.Tagged (Tagged (..))
11−import Servant ((:<|>) (..), ServerError(..))
11+import Servant ((:<|>) (..), ServerError(..), NamedRoutes(..))
12+import Servant.API.Generic
13+import Servant.Server.Generic
14+import Servant.Server
1215import Network.HTTP.Types
1316import Network.Wai
1417
@@ -26,6 +29,12 @@ class ThrowAll a where
2629instance (ThrowAll a, ThrowAll b) => ThrowAll (a :<|> b) where
2730 throwAll e = throwAll e :<|> throwAll e
2831
32+instance
33+ ( ThrowAll (ToServant api (AsServerT m)) , GenericServant api (AsServerT m)) =>
34+ ThrowAll (api (AsServerT m)) where
35+
36+ throwAll = fromServant . throwAll
37+
2938-- Really this shouldn't be necessary - ((->) a) should be an instance of
3039-- MonadError, no?
3140instance {-# OVERLAPPING #-} ThrowAll b => ThrowAll (a -> b) where
servant-auth/servant-auth-server/test/Servant/Auth/ServerSpec.hs+7
@@ -50,6 +50,7 @@ import Network.Wreq (Options, auth, basicAuth,
5050import Network.Wreq.Types (Postable(..))
5151import Servant hiding (BasicAuth,
5252 IsSecure (..), header)
53+import Servant.API.Generic ((:-))
5354import Servant.Auth.Server
5455import Servant.Auth.Server.Internal.Cookie (expireTime)
5556import Servant.Auth.Server.SetCookieOrphan ()
@@ -405,6 +406,7 @@ type API auths
405406 = Auth auths User :>
406407 ( Get '[JSON] Int
407408 :<|> ReqBody '[JSON] Int :> Post '[JSON] Int
409+ :<|> NamedRoutes DummyRoutes
408410 :<|> "header" :> Get '[JSON] (Headers '[Header "Blah" Int] Int)
409411#if MIN_VERSION_servant_server(0,15,0)
410412 :<|> "stream" :> StreamGet NoFraming OctetStream (SourceIO BS.ByteString)
@@ -416,6 +418,10 @@ type API auths
416418 :<|> "logout" :> Get '[JSON] (Headers '[ Header "Set-Cookie" SetCookie
417419 , Header "Set-Cookie" SetCookie ] NoContent)
418420
421+data DummyRoutes mode = DummyRoutes
422+ { dummyInt :: mode :- "dummy" :> Get '[JSON] Int
423+ } deriving Generic
424+
419425jwtOnlyApi :: Proxy (API '[Servant.Auth.Server.JWT])
420426jwtOnlyApi = Proxy
421427
@@ -476,6 +482,7 @@ server ccfg =
476482 (\authResult -> case authResult of
477483 Authenticated usr -> getInt usr
478484 :<|> postInt usr
485+ :<|> DummyRoutes { dummyInt = getInt usr }
479486 :<|> getHeaderInt
480487#if MIN_VERSION_servant_server(0,15,0)
481488 :<|> return (S.source ["bytestring"])
servant-auth/servant-auth-swagger/servant-auth-swagger.cabal+12−16
@@ -1,3 +1,4 @@
1+cabal-version: 2.2
12name: servant-auth-swagger
23version: 0.2.10.1
34synopsis: servant-swagger/servant-auth compatibility
@@ -6,9 +7,9 @@ description: This package provides instances that allow generating swagger2 s
67 APIs that use
78 <https://hackage.haskell.org/package/servant-auth servant-auth's> @Auth@ combinator.
89 .
9− For a quick overview of the usage, see the <http://github.com/haskell-servant/servant/servant-auth#readme README>.
10+ For a quick overview of the usage, see the <https://github.com/haskell-servant/servant/tree/master/servant-auth#readme README>.
1011category: Web, Servant, Authentication
11−homepage: http://github.com/haskell-servant/servant/servant-auth#readme
12+homepage: https://github.com/haskell-servant/servant/tree/master/servant-auth#readme
1213bug-reports: https://github.com/haskell-servant/servant/issues
1314author: Julian K. Arni
1415maintainer: jkarni@gmail.com
@@ -17,7 +18,6 @@ license: BSD-3-Clause
1718license-file: LICENSE
1819tested-with: GHC ==8.6.5 || ==8.8.4 || ==8.10.4
1920build-type: Simple
20−cabal-version: >= 1.10
2121extra-source-files:
2222 CHANGELOG.md
2323
@@ -31,15 +31,13 @@ library
3131 default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable FlexibleContexts FlexibleInstances FunctionalDependencies GADTs KindSignatures MultiParamTypeClasses OverloadedStrings RankNTypes ScopedTypeVariables TypeFamilies TypeOperators
3232 ghc-options: -Wall
3333 build-depends:
34− base >= 4.10 && < 4.16
35− , text >= 1.2.3.0 && < 1.3
36− , servant-swagger >= 1.1.5 && < 1.8
37− , swagger2 >= 2.2.2 && < 2.7
38− , servant >= 0.13 && < 0.19
34+ base >= 4.10 && < 4.17
35+ , text >= 1.2.3.0 && < 2.1
36+ , servant-swagger >= 1.1.5 && < 2
37+ , swagger2 >= 2.2.2 && < 3
38+ , servant >= 0.13 && < 0.20
3939 , servant-auth == 0.4.*
40− , lens >= 4.16.1 && < 5.1
41− if impl(ghc >= 9)
42− buildable: False
40+ , lens >= 4.16.1 && < 5.2
4341 exposed-modules:
4442 Servant.Auth.Swagger
4543 default-language: Haskell2010
@@ -51,7 +49,7 @@ test-suite spec
5149 test
5250 default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable FlexibleContexts FlexibleInstances FunctionalDependencies GADTs KindSignatures MultiParamTypeClasses OverloadedStrings RankNTypes ScopedTypeVariables TypeFamilies TypeOperators
5351 ghc-options: -Wall
54− build-tool-depends: hspec-discover:hspec-discover >= 2.5.5 && <2.9
52+ build-tool-depends: hspec-discover:hspec-discover >= 2.5.5 && <2.10
5553 -- dependencies with bounds inherited from the library stanza
5654 build-depends:
5755 base
@@ -61,13 +59,11 @@ test-suite spec
6159 , servant
6260 , servant-auth
6361 , lens
64− if impl(ghc >= 9)
65− buildable: False
66−
62+
6763 -- test dependencies
6864 build-depends:
6965 servant-auth-swagger
70− , hspec >= 2.5.5 && < 2.9
66+ , hspec >= 2.5.5 && < 2.10
7167 , QuickCheck >= 2.11.3 && < 2.15
7268 other-modules:
7369 Servant.Auth.SwaggerSpec
servant-auth/servant-auth/servant-auth.cabal+11−10
@@ -1,5 +1,6 @@
1+cabal-version: 2.2
12name: servant-auth
2−version: 0.4.0.0
3+version: 0.4.1.0
34synopsis: Authentication combinators for servant
45description: This package provides an @Auth@ combinator for 'servant'. This combinator
56 allows using different authentication schemes in a straightforward way,
@@ -8,9 +9,9 @@ description: This package provides an @Auth@ combinator for 'servant'. This c
89 'servant-auth' additionally provides concrete authentication schemes, such
910 as Basic Access Authentication, JSON Web Tokens, and Cookies.
1011 .
11− For more details on how to use this, see the <http://github.com/haskell-servant/servant/servant-auth#readme README>.
12+ For more details on how to use this, see the <https://github.com/haskell-servant/servant/tree/master/servant-auth#readme README>.
1213category: Web, Servant, Authentication
13−homepage: http://github.com/haskell-servant/servant/servant-auth#readme
14+homepage: https://github.com/haskell-servant/servant/tree/master/servant-auth#readme
1415bug-reports: https://github.com/haskell-servant/servant/issues
1516author: Julian K. Arni
1617maintainer: jkarni@gmail.com
@@ -19,7 +20,6 @@ license: BSD-3-Clause
1920license-file: LICENSE
2021tested-with: GHC ==8.6.5 || ==8.8.4 || ==8.10.4 || ==9.0.1
2122build-type: Simple
22−cabal-version: >= 1.10
2323extra-source-files:
2424 CHANGELOG.md
2525
@@ -33,12 +33,13 @@ library
3333 default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable FlexibleContexts FlexibleInstances FunctionalDependencies GADTs KindSignatures MultiParamTypeClasses OverloadedStrings RankNTypes ScopedTypeVariables TypeFamilies TypeOperators
3434 ghc-options: -Wall
3535 build-depends:
36− base >= 4.10 && < 4.16
37− , aeson >= 1.3.1.1 && < 1.6
38− , jose >= 0.7.0.0 && < 0.9
39− , lens >= 4.16.1 && < 5.1
40− , servant >= 0.15 && < 0.19
41− , text >= 1.2.3.0 && < 1.3
36+ base >= 4.10 && < 4.17
37+ , containers >= 0.6 && < 0.7
38+ , aeson >= 1.3.1.1 && < 3
39+ , jose >= 0.7.0.0 && < 0.10
40+ , lens >= 4.16.1 && < 5.2
41+ , servant >= 0.15 && < 0.20
42+ , text >= 1.2.3.0 && < 2.1
4243 , unordered-containers >= 0.2.9.0 && < 0.3
4344 exposed-modules:
4445 Servant.Auth
servant-auth/servant-auth/src/Servant/Auth/JWT.hs+9−2
@@ -1,10 +1,17 @@
1+{-# LANGUAGE CPP #-}
2+
13module Servant.Auth.JWT where
24
35import Control.Lens ((^.))
46import qualified Crypto.JWT as Jose
57import Data.Aeson (FromJSON, Result (..), ToJSON, fromJSON,
68 toJSON)
7−import qualified Data.HashMap.Strict as HM
9+#if MIN_VERSION_aeson(2,0,0)
10+import qualified Data.Map as KM
11+#else
12+import qualified Data.HashMap.Strict as KM
13+#endif
14+
815import qualified Data.Text as T
916
1017
@@ -17,7 +24,7 @@ import qualified Data.Text as T
1724class FromJWT a where
1825 decodeJWT :: Jose.ClaimsSet -> Either T.Text a
1926 default decodeJWT :: FromJSON a => Jose.ClaimsSet -> Either T.Text a
20− decodeJWT m = case HM.lookup "dat" (m ^. Jose.unregisteredClaims) of
27+ decodeJWT m = case KM.lookup "dat" (m ^. Jose.unregisteredClaims) of
2128 Nothing -> Left "Missing 'dat' claim"
2229 Just v -> case fromJSON v of
2330 Error e -> Left $ T.pack e
servant-client-core/CHANGELOG.md+31
@@ -1,6 +1,37 @@
11[The latest version of this document is on GitHub.](https://github.com/haskell-servant/servant/blob/master/servant-client-core/CHANGELOG.md)
22[Changelog for `servant` package contains significant entries for all core packages.](https://github.com/haskell-servant/servant/blob/master/servant/CHANGELOG.md)
33
4+Package versions follow the [Package Versioning Policy](https://pvp.haskell.org/): in A.B.C, bumps to either A or B represent major versions.
5+
6+0.19
7+----
8+
9+### Significant changes
10+
11+- Drop support for GHC < 8.6.
12+- Support GHC 9.0 (GHC 9.2 should work as well, but isn't fully tested yet).
13+- Support Aeson 2 ([#1475](https://github.com/haskell-servant/servant/pull/1475)),
14+ which fixes a [DOS vulnerability](https://github.com/haskell/aeson/issues/864)
15+ related to hash collisions.
16+- Add `NamedRoutes` combinator, making support for records first-class in Servant
17+ ([#1388](https://github.com/haskell-servant/servant/pull/1388)).
18+- Add custom type errors for partially applied combinators
19+ ([#1289](https://github.com/haskell-servant/servant/pull/1289),
20+ [#1486](https://github.com/haskell-servant/servant/pull/1486)).
21+- *servant-client* / *servant-client-core* / *servant-http-streams*: Fix
22+ erroneous behavior, where only 2XX status codes would be considered
23+ successful, irrelevant of the status parameter specified by the verb
24+ combinator. ([#1469](https://github.com/haskell-servant/servant/pull/1469))
25+- *servant-client* / *servant-client-core*: Fix `Show` instance for
26+ `Servant.Client.Core.Request`.
27+- *servant-client* / *servant-client-core*: Allow passing arbitrary binary data
28+ in Query parameters.
29+ ([#1432](https://github.com/haskell-servant/servant/pull/1432)).
30+
31+### Other changes
32+
33+- Various version bumps.
34+
4350.18.3
536------
637
servant-client-core/servant-client-core.cabal+11−10
@@ -1,6 +1,6 @@
1−cabal-version: >=1.10
1+cabal-version: 2.2
22name: servant-client-core
3−version: 0.18.3
3+version: 0.19
44
55synopsis: Core functionality and class for client function generation for servant APIs
66category: Servant, Web
@@ -50,13 +50,14 @@ library
5050 --
5151 -- note: mtl lower bound is so low because of GHC-7.8
5252 build-depends:
53− base >= 4.9 && < 4.16
53+ base >= 4.9 && < 4.17
5454 , bytestring >= 0.10.8.1 && < 0.12
55+ , constraints >= 0.2 && < 0.14
5556 , containers >= 0.5.7.1 && < 0.7
5657 , deepseq >= 1.4.2.0 && < 1.5
57− , text >= 1.2.3.0 && < 1.3
58+ , text >= 1.2.3.0 && < 2.1
5859 , transformers >= 0.5.2.0 && < 0.6
59− , template-haskell >= 2.11.1.0 && < 2.18
60+ , template-haskell >= 2.11.1.0 && < 2.19
6061
6162 if !impl(ghc >= 8.2)
6263 build-depends:
@@ -64,13 +65,13 @@ library
6465
6566 -- Servant dependencies
6667 build-depends:
67− servant >= 0.18.3 && <0.19
68+ servant >= 0.19
6869
6970 -- Other dependencies: Lower bound around what is in the latest Stackage LTS.
7071 -- Here can be exceptions if we really need features from the newer versions.
7172 build-depends:
72− aeson >= 1.4.1.0 && < 1.6
73− , base-compat >= 0.10.5 && < 0.12
73+ aeson >= 1.4.1.0 && < 3
74+ , base-compat >= 0.10.5 && < 0.13
7475 , base64-bytestring >= 1.0.0.1 && < 1.3
7576 , exceptions >= 0.10.0 && < 0.11
7677 , free >= 5.1 && < 5.2
@@ -103,8 +104,8 @@ test-suite spec
103104 -- Additional dependencies
104105 build-depends:
105106 deepseq >= 1.4.2.0 && < 1.5
106− , hspec >= 2.6.0 && < 2.9
107+ , hspec >= 2.6.0 && < 2.10
107108 , QuickCheck >= 2.12.6.1 && < 2.15
108109
109110 build-tool-depends:
110− hspec-discover:hspec-discover >= 2.6.0 && <2.9
111+ hspec-discover:hspec-discover >= 2.6.0 && <2.10
servant-client-core/src/Servant/Client/Core.hs+1
@@ -59,6 +59,7 @@ module Servant.Client.Core
5959 , appendToPath
6060 , setRequestBodyLBS
6161 , setRequestBody
62+ , encodeQueryParamValue
6263 ) where
6364import Servant.Client.Core.Auth
6465import Servant.Client.Core.BaseUrl
servant-client-core/src/Servant/Client/Core/HasClient.hs+170−36
@@ -1,5 +1,4 @@
11{-# LANGUAGE ConstraintKinds #-}
2−{-# LANGUAGE CPP #-}
32{-# LANGUAGE DataKinds #-}
43{-# LANGUAGE FlexibleContexts #-}
54{-# LANGUAGE FlexibleInstances #-}
@@ -7,6 +6,7 @@
76{-# LANGUAGE MultiParamTypeClasses #-}
87{-# LANGUAGE OverloadedStrings #-}
98{-# LANGUAGE PolyKinds #-}
9+{-# LANGUAGE QuantifiedConstraints #-}
1010{-# LANGUAGE RankNTypes #-}
1111{-# LANGUAGE ScopedTypeVariables #-}
1212{-# LANGUAGE TypeApplications #-}
@@ -14,14 +14,13 @@
1414{-# LANGUAGE TypeOperators #-}
1515{-# LANGUAGE UndecidableInstances #-}
1616
17−#if MIN_VERSION_base(4,9,0) && __GLASGOW_HASKELL__ >= 802
18−#define HAS_TYPE_ERROR
19−#endif
20−
2117module Servant.Client.Core.HasClient (
2218 clientIn,
2319 HasClient (..),
2420 EmptyClient (..),
21+ AsClientT,
22+ (//),
23+ (/:),
2524 foldMapUnion,
2625 matchUnion,
2726 ) where
@@ -34,11 +33,10 @@ import Control.Arrow
3433import Control.Monad
3534 (unless)
3635import qualified Data.ByteString as BS
37−import Data.ByteString.Builder
38− (toLazyByteString)
39−import qualified Data.ByteString.Lazy as BL
36+import qualified Data.ByteString.Lazy as BL
4037import Data.Either
4138 (partitionEithers)
39+import Data.Constraint (Dict(..))
4240import Data.Foldable
4341 (toList)
4442import Data.List
@@ -47,7 +45,8 @@ import Data.Sequence
4745 (fromList)
4846import qualified Data.Text as T
4947import Network.HTTP.Media
50− (MediaType, matches, parseAccept, (//))
48+ (MediaType, matches, parseAccept)
49+import qualified Network.HTTP.Media as Media
5150import qualified Data.Sequence as Seq
5251import Data.SOP.BasicFunctors
5352 (I (I), (:.:) (Comp))
@@ -64,7 +63,7 @@ import Data.Text
6463import Data.Proxy
6564 (Proxy (Proxy))
6665import GHC.TypeLits
67− (KnownSymbol, symbolVal)
66+ (KnownNat, KnownSymbol, TypeError, symbolVal)
6867import Network.HTTP.Types
6968 (Status)
7069import qualified Network.HTTP.Types as H
@@ -79,12 +78,18 @@ import Servant.API
7978 ReflectMethod (..), RemoteHost, ReqBody', SBoolI, Stream,
8079 StreamBody', Summary, ToHttpApiData, ToSourceIO (..), Vault,
8180 Verb, WithNamedContext, WithStatus (..), contentType, getHeadersHList,
82− getResponse, toEncodedUrlPiece, toUrlPiece)
81+ getResponse, toEncodedUrlPiece, toUrlPiece, NamedRoutes)
82+import Servant.API.Generic
83+ (GenericMode(..), ToServant, ToServantApi
84+ , GenericServant, toServant, fromServant)
8385import Servant.API.ContentTypes
8486 (contentTypes, AllMime (allMime), AllMimeUnrender (allMimeUnrender))
87+import Servant.API.Status
88+ (statusFromNat)
8589import Servant.API.TypeLevel (FragmentUnique, AtLeastOneFragment)
8690import Servant.API.Modifiers
8791 (FoldRequired, RequiredArgument, foldRequiredArgument)
92+import Servant.API.TypeErrors
8893import Servant.API.UVerb
8994 (HasStatus, HasStatuses (Statuses, statuses), UVerb, Union, Unique, inject, statusOf, foldMapUnion, matchUnion)
9095
@@ -203,7 +208,7 @@ instance (KnownSymbol capture, ToHttpApiData a, HasClient m api)
203208 clientWithRoute pm (Proxy :: Proxy api)
204209 (appendToPath p req)
205210
206− where p = (toUrlPiece val)
211+ where p = toEncodedUrlPiece val
207212
208213 hoistClientMonad pm _ f cl = \a ->
209214 hoistClientMonad pm (Proxy :: Proxy api) f (cl a)
@@ -238,7 +243,7 @@ instance (KnownSymbol capture, ToHttpApiData a, HasClient m sublayout)
238243 clientWithRoute pm (Proxy :: Proxy sublayout)
239244 (foldl' (flip appendToPath) req ps)
240245
241− where ps = map (toUrlPiece) vals
246+ where ps = map toEncodedUrlPiece vals
242247
243248 hoistClientMonad pm _ f cl = \as ->
244249 hoistClientMonad pm (Proxy :: Proxy sublayout) f (cl as)
@@ -246,10 +251,11 @@ instance (KnownSymbol capture, ToHttpApiData a, HasClient m sublayout)
246251instance {-# OVERLAPPABLE #-}
247252 -- Note [Non-Empty Content Types]
248253 ( RunClient m, MimeUnrender ct a, ReflectMethod method, cts' ~ (ct ': cts)
254+ , KnownNat status
249255 ) => HasClient m (Verb method status cts' a) where
250256 type Client m (Verb method status cts' a) = m a
251257 clientWithRoute _pm Proxy req = do
252− response <- runRequest req
258+ response <- runRequestAcceptStatus (Just [status]) req
253259 { requestAccept = fromList $ toList accept
254260 , requestMethod = method
255261 }
@@ -257,18 +263,20 @@ instance {-# OVERLAPPABLE #-}
257263 where
258264 accept = contentTypes (Proxy :: Proxy ct)
259265 method = reflectMethod (Proxy :: Proxy method)
266+ status = statusFromNat (Proxy :: Proxy status)
260267
261268 hoistClientMonad _ _ f ma = f ma
262269
263270instance {-# OVERLAPPING #-}
264− ( RunClient m, ReflectMethod method
271+ ( RunClient m, ReflectMethod method, KnownNat status
265272 ) => HasClient m (Verb method status cts NoContent) where
266273 type Client m (Verb method status cts NoContent)
267274 = m NoContent
268275 clientWithRoute _pm Proxy req = do
269− _response <- runRequest req { requestMethod = method }
276+ _response <- runRequestAcceptStatus (Just [status]) req { requestMethod = method }
270277 return NoContent
271278 where method = reflectMethod (Proxy :: Proxy method)
279+ status = statusFromNat (Proxy :: Proxy status)
272280
273281 hoistClientMonad _ _ f ma = f ma
274282
@@ -285,13 +293,13 @@ instance (RunClient m, ReflectMethod method) =>
285293
286294instance {-# OVERLAPPING #-}
287295 -- Note [Non-Empty Content Types]
288− ( RunClient m, MimeUnrender ct a, BuildHeadersTo ls
296+ ( RunClient m, MimeUnrender ct a, BuildHeadersTo ls, KnownNat status
289297 , ReflectMethod method, cts' ~ (ct ': cts)
290298 ) => HasClient m (Verb method status cts' (Headers ls a)) where
291299 type Client m (Verb method status cts' (Headers ls a))
292300 = m (Headers ls a)
293301 clientWithRoute _pm Proxy req = do
294− response <- runRequest req
302+ response <- runRequestAcceptStatus (Just [status]) req
295303 { requestMethod = method
296304 , requestAccept = fromList $ toList accept
297305 }
@@ -299,22 +307,26 @@ instance {-# OVERLAPPING #-}
299307 return $ Headers { getResponse = val
300308 , getHeadersHList = buildHeadersTo . toList $ responseHeaders response
301309 }
302− where method = reflectMethod (Proxy :: Proxy method)
303− accept = contentTypes (Proxy :: Proxy ct)
310+ where
311+ method = reflectMethod (Proxy :: Proxy method)
312+ accept = contentTypes (Proxy :: Proxy ct)
313+ status = statusFromNat (Proxy :: Proxy status)
304314
305315 hoistClientMonad _ _ f ma = f ma
306316
307317instance {-# OVERLAPPING #-}
308− ( RunClient m, BuildHeadersTo ls, ReflectMethod method
318+ ( RunClient m, BuildHeadersTo ls, ReflectMethod method, KnownNat status
309319 ) => HasClient m (Verb method status cts (Headers ls NoContent)) where
310320 type Client m (Verb method status cts (Headers ls NoContent))
311321 = m (Headers ls NoContent)
312322 clientWithRoute _pm Proxy req = do
313− let method = reflectMethod (Proxy :: Proxy method)
314− response <- runRequest req { requestMethod = method }
323+ response <- runRequestAcceptStatus (Just [status]) req { requestMethod = method }
315324 return $ Headers { getResponse = NoContent
316325 , getHeadersHList = buildHeadersTo . toList $ responseHeaders response
317326 }
327+ where
328+ method = reflectMethod (Proxy :: Proxy method)
329+ status = statusFromNat (Proxy :: Proxy status)
318330
319331 hoistClientMonad _ _ f ma = f ma
320332
@@ -557,7 +569,7 @@ instance (KnownSymbol sym, ToHttpApiData a, HasClient m api, SBoolI (FoldRequire
557569 (Proxy :: Proxy mods) add (maybe req add) mparam
558570 where
559571 add :: a -> Request
560− add param = appendToQueryString pname (Just $ encodeQueryParam param) req
572+ add param = appendToQueryString pname (Just $ encodeQueryParamValue param) req
561573
562574 pname :: Text
563575 pname = pack $ symbolVal (Proxy :: Proxy sym)
@@ -565,9 +577,6 @@ instance (KnownSymbol sym, ToHttpApiData a, HasClient m api, SBoolI (FoldRequire
565577 hoistClientMonad pm _ f cl = \arg ->
566578 hoistClientMonad pm (Proxy :: Proxy api) f (cl arg)
567579
568−encodeQueryParam :: ToHttpApiData a => a -> BS.ByteString
569−encodeQueryParam = BL.toStrict . toLazyByteString . toEncodedUrlPiece
570−
571580-- | If you use a 'QueryParams' in one of your endpoints in your API,
572581-- the corresponding querying function will automatically take
573582-- an additional argument, a list of values of the type specified
@@ -609,7 +618,7 @@ instance (KnownSymbol sym, ToHttpApiData a, HasClient m api)
609618 )
610619
611620 where pname = pack $ symbolVal (Proxy :: Proxy sym)
612− paramlist' = map (Just . encodeQueryParam) paramlist
621+ paramlist' = map (Just . encodeQueryParamValue) paramlist
613622
614623 hoistClientMonad pm _ f cl = \as ->
615624 hoistClientMonad pm (Proxy :: Proxy api) f (cl as)
@@ -731,7 +740,7 @@ instance (KnownSymbol path, HasClient m api) => HasClient m (path :> api) where
731740 clientWithRoute pm (Proxy :: Proxy api)
732741 (appendToPath p req)
733742
734− where p = pack $ symbolVal (Proxy :: Proxy path)
743+ where p = toEncodedUrlPiece $ pack $ symbolVal (Proxy :: Proxy path)
735744
736745 hoistClientMonad pm _ f cl = hoistClientMonad pm (Proxy :: Proxy api) f cl
737746
@@ -780,7 +789,7 @@ instance ( HasClient m api
780789
781790-- | Ignore @'Fragment'@ in client functions.
782791-- See <https://ietf.org/rfc/rfc2616.html#section-15.1.3> for more details.
783−--
792+--
784793-- Example:
785794--
786795-- > type MyApi = "books" :> Fragment Text :> Get '[JSON] [Book]
@@ -792,16 +801,12 @@ instance ( HasClient m api
792801-- > getBooks = client myApi
793802-- > -- then you can just use "getBooksBy" to query that endpoint.
794803-- > -- 'getBooks' for all books.
795−#ifdef HAS_TYPE_ERROR
796804instance (AtLeastOneFragment api, FragmentUnique (Fragment a :> api), HasClient m api
797−#else
798−instance ( HasClient m api
799−#endif
800805 ) => HasClient m (Fragment a :> api) where
801806
802807 type Client m (Fragment a :> api) = Client m api
803808
804− clientWithRoute pm _ = clientWithRoute pm (Proxy :: Proxy api)
809+ clientWithRoute pm _ = clientWithRoute pm (Proxy :: Proxy api)
805810
806811 hoistClientMonad pm _ = hoistClientMonad pm (Proxy :: Proxy api)
807812
@@ -816,6 +821,119 @@ instance HasClient m api => HasClient m (BasicAuth realm usr :> api) where
816821 hoistClientMonad pm _ f cl = \bauth ->
817822 hoistClientMonad pm (Proxy :: Proxy api) f (cl bauth)
818823
824+-- | A type that specifies that an API record contains a client implementation.
825+data AsClientT (m :: * -> *)
826+instance GenericMode (AsClientT m) where
827+ type AsClientT m :- api = Client m api
828+
829+
830+type GClientConstraints api m =
831+ ( GenericServant api (AsClientT m)
832+ , Client m (ToServantApi api) ~ ToServant api (AsClientT m)
833+ )
834+
835+class GClient (api :: * -> *) m where
836+ gClientProof :: Dict (GClientConstraints api m)
837+
838+instance GClientConstraints api m => GClient api m where
839+ gClientProof = Dict
840+
841+instance
842+ ( forall n. GClient api n
843+ , HasClient m (ToServantApi api)
844+ , RunClient m
845+ )
846+ => HasClient m (NamedRoutes api) where
847+ type Client m (NamedRoutes api) = api (AsClientT m)
848+
849+ clientWithRoute :: Proxy m -> Proxy (NamedRoutes api) -> Request -> Client m (NamedRoutes api)
850+ clientWithRoute pm _ request =
851+ case gClientProof @api @m of
852+ Dict -> fromServant $ clientWithRoute pm (Proxy @(ToServantApi api)) request
853+
854+ hoistClientMonad
855+ :: forall ma mb.
856+ Proxy m
857+ -> Proxy (NamedRoutes api)
858+ -> (forall x. ma x -> mb x)
859+ -> Client ma (NamedRoutes api)
860+ -> Client mb (NamedRoutes api)
861+ hoistClientMonad _ _ nat clientA =
862+ case (gClientProof @api @ma, gClientProof @api @mb) of
863+ (Dict, Dict) ->
864+ fromServant @api @(AsClientT mb) $
865+ hoistClientMonad @m @(ToServantApi api) @ma @mb Proxy Proxy nat $
866+ toServant @api @(AsClientT ma) clientA
867+
868+infixl 1 //
869+infixl 2 /:
870+
871+-- | Helper to make code using records of clients more readable.
872+--
873+-- Can be mixed with (/:) for supplying arguments.
874+--
875+-- Example:
876+--
877+-- @
878+-- type Api = NamedRoutes RootApi
879+--
880+-- data RootApi mode = RootApi
881+-- { subApi :: mode :- NamedRoutes SubApi
882+-- , …
883+-- } deriving Generic
884+--
885+-- data SubApi mode = SubApi
886+-- { endpoint :: mode :- Get '[JSON] Person
887+-- , …
888+-- } deriving Generic
889+--
890+-- api :: Proxy API
891+-- api = Proxy
892+--
893+-- rootClient :: RootApi (AsClientT ClientM)
894+-- rootClient = client api
895+--
896+-- endpointClient :: ClientM Person
897+-- endpointClient = client // subApi // endpoint
898+-- @
899+(//) :: a -> (a -> b) -> b
900+x // f = f x
901+
902+-- | Convenience function for supplying arguments to client functions when
903+-- working with records of clients.
904+--
905+-- Intended to be used in conjunction with '(//)'.
906+--
907+-- Example:
908+--
909+-- @
910+-- type Api = NamedRoutes RootApi
911+--
912+-- data RootApi mode = RootApi
913+-- { subApi :: mode :- Capture "token" String :> NamedRoutes SubApi
914+-- , hello :: mode :- Capture "name" String :> Get '[JSON] String
915+-- , …
916+-- } deriving Generic
917+--
918+-- data SubApi mode = SubApi
919+-- { endpoint :: mode :- Get '[JSON] Person
920+-- , …
921+-- } deriving Generic
922+--
923+-- api :: Proxy API
924+-- api = Proxy
925+--
926+-- rootClient :: RootApi (AsClientT ClientM)
927+-- rootClient = client api
928+--
929+-- hello :: String -> ClientM String
930+-- hello name = rootClient // hello /: name
931+--
932+-- endpointClient :: ClientM Person
933+-- endpointClient = client // subApi /: "foobar123" // endpoint
934+-- @
935+(/:) :: (a -> b -> c) -> b -> a -> c
936+(/:) = flip
819937
820938
821939{- Note [Non-Empty Content Types]
@@ -841,7 +959,7 @@ for empty and one for non-empty lists).
841959checkContentTypeHeader :: RunClient m => Response -> m MediaType
842960checkContentTypeHeader response =
843961 case lookup "Content-Type" $ toList $ responseHeaders response of
844− Nothing -> return $ "application"//"octet-stream"
962+ Nothing -> return $ "application" Media.// "octet-stream"
845963 Just t -> case parseAccept t of
846964 Nothing -> throwClientError $ InvalidContentTypeHeader response
847965 Just t' -> return t'
@@ -857,3 +975,19 @@ decodedAs response ct = do
857975 Right val -> return val
858976 where
859977 accept = toList $ contentTypes ct
978+
979+-------------------------------------------------------------------------------
980+-- Custom type errors
981+-------------------------------------------------------------------------------
982+
983+-- Erroring instance for HasClient' when a combinator is not fully applied
984+instance (RunClient m, TypeError (PartialApplication HasClient arr)) => HasClient m ((arr :: a -> b) :> sub)
985+ where
986+ type Client m (arr :> sub) = TypeError (PartialApplication HasClient arr)
987+ clientWithRoute _ _ _ = error "unreachable"
988+ hoistClientMonad _ _ _ _ = error "unreachable"
989+
990+-- Erroring instances for 'HasClient' for unknown API combinators
991+instance {-# OVERLAPPABLE #-} (RunClient m, TypeError (NoInstanceForSub (HasClient m) ty)) => HasClient m (ty :> sub)
992+
993+instance {-# OVERLAPPABLE #-} (RunClient m, TypeError (NoInstanceFor (HasClient m api))) => HasClient m api
servant-client-core/src/Servant/Client/Core/Reexport.hs+4
@@ -7,6 +7,9 @@ module Servant.Client.Core.Reexport
77 HasClient(..)
88 , foldMapUnion
99 , matchUnion
10+ , AsClientT
11+ , (//)
12+ , (/:)
1013
1114 -- * Response (for @Raw@)
1215 , Response
@@ -23,6 +26,7 @@ module Servant.Client.Core.Reexport
2326 , showBaseUrl
2427 , parseBaseUrl
2528 , InvalidBaseUrlException
29+
2630 ) where
2731
2832
servant-client-core/src/Servant/Client/Core/Request.hs+25−8
@@ -17,6 +17,7 @@ module Servant.Client.Core.Request (
1717 addHeader,
1818 appendToPath,
1919 appendToQueryString,
20+ encodeQueryParamValue,
2021 setRequestBody,
2122 setRequestBodyLBS,
2223 ) where
@@ -33,6 +34,8 @@ import Data.Bifunctor
3334import Data.Bitraversable
3435 (Bitraversable (..), bifoldMapDefault, bimapDefault)
3536import qualified Data.ByteString as BS
37+import Data.ByteString.Builder
38+ (Builder)
3639import qualified Data.ByteString.Builder as Builder
3740import qualified Data.ByteString.Lazy as LBS
3841import qualified Data.Sequence as Seq
@@ -48,9 +51,9 @@ import Network.HTTP.Media
4851 (MediaType)
4952import Network.HTTP.Types
5053 (Header, HeaderName, HttpVersion (..), Method, QueryItem,
51− http11, methodGet)
54+ http11, methodGet, urlEncodeBuilder)
5255import Servant.API
53− (ToHttpApiData, toEncodedUrlPiece, toHeader, SourceIO)
56+ (ToHttpApiData, toEncodedUrlPiece, toQueryParam, toHeader, SourceIO)
5457
5558import Servant.Client.Core.Internal (mediaTypeRnf)
5659
@@ -78,12 +81,13 @@ instance (Show a, Show b) =>
7881 . showString ", requestAccept = "
7982 . showsPrec 0 (requestAccept req)
8083 . showString ", requestHeaders = "
81− . showsPrec 0 (redactSensitiveHeader <$> requestHeaders req))
84+ . showsPrec 0 (redactSensitiveHeader <$> requestHeaders req)
8285 . showString ", requestHttpVersion = "
8386 . showsPrec 0 (requestHttpVersion req)
8487 . showString ", requestMethod = "
8588 . showsPrec 0 (requestMethod req)
8689 . showString "}"
90+ )
8791 where
8892 redactSensitiveHeader :: Header -> Header
8993 redactSensitiveHeader ("Authorization", _) = ("Authorization", "<REDACTED>")
@@ -110,7 +114,7 @@ instance (NFData path, NFData body) => NFData (RequestF body path) where
110114 rnfB Nothing = ()
111115 rnfB (Just (b, mt)) = rnf b `seq` mediaTypeRnf mt
112116
113−type Request = RequestF RequestBody Builder.Builder
117+type Request = RequestF RequestBody Builder
114118
115119-- | The request body. R replica of the @http-client@ @RequestBody@.
116120data RequestBody
@@ -141,18 +145,31 @@ defaultRequest = Request
141145 , requestMethod = methodGet
142146 }
143147
144−appendToPath :: Text -> Request -> Request
148+-- | Append extra path to the request being constructed.
149+--
150+-- Warning: This function assumes that the path fragment is already URL-encoded.
151+appendToPath :: Builder -> Request -> Request
145152appendToPath p req
146− = req { requestPath = requestPath req <> "/" <> toEncodedUrlPiece p }
153+ = req { requestPath = requestPath req <> "/" <> p }
147154
148−appendToQueryString :: Text -- ^ param name
149− -> Maybe BS.ByteString -- ^ param value
155+-- | Append a query parameter to the request being constructed.
156+--
157+appendToQueryString :: Text -- ^ query param name
158+ -> Maybe BS.ByteString -- ^ query param value
150159 -> Request
151160 -> Request
152161appendToQueryString pname pvalue req
153162 = req { requestQueryString = requestQueryString req
154163 Seq.|> (encodeUtf8 pname, pvalue)}
155164
165+-- | Encode a query parameter value.
166+--
167+encodeQueryParamValue :: ToHttpApiData a => a -> BS.ByteString
168+encodeQueryParamValue = LBS.toStrict . Builder.toLazyByteString
169+ . urlEncodeBuilder True . encodeUtf8 . toQueryParam
170+
171+-- | Add header to the request being constructed.
172+--
156173addHeader :: ToHttpApiData a => HeaderName -> a -> Request -> Request
157174addHeader name val req
158175 = req { requestHeaders = requestHeaders req Seq.|> (name, toHeader val)}
servant-client-core/src/Servant/Client/Generic.hs+8−11
@@ -1,9 +1,10 @@
1−{-# LANGUAGE ConstraintKinds #-}
2−{-# LANGUAGE FlexibleContexts #-}
3−{-# LANGUAGE KindSignatures #-}
4−{-# LANGUAGE RankNTypes #-}
5−{-# LANGUAGE ScopedTypeVariables #-}
6−{-# LANGUAGE TypeFamilies #-}
1+{-# OPTIONS_GHC -fno-warn-orphans #-}
2+{-# LANGUAGE FlexibleContexts #-}
3+{-# LANGUAGE KindSignatures #-}
4+{-# LANGUAGE RankNTypes #-}
5+{-# LANGUAGE ScopedTypeVariables #-}
6+{-# LANGUAGE TypeFamilies #-}
7+
78module Servant.Client.Generic (
89 AsClientT,
910 genericClient,
@@ -15,11 +16,7 @@ import Data.Proxy
1516
1617import Servant.API.Generic
1718import Servant.Client.Core
18−
19−-- | A type that specifies that an API record contains a client implementation.
20−data AsClientT (m :: * -> *)
21−instance GenericMode (AsClientT m) where
22− type AsClientT m :- api = Client m api
19+import Servant.Client.Core.HasClient (AsClientT)
2320
2421-- | Generate a record of client functions.
2522genericClient
servant-client-core/test/Servant/Client/Core/RequestSpec.hs+12
@@ -10,10 +10,22 @@ import Data.List (isInfixOf)
1010import Servant.Client.Core.Request
1111import Test.Hspec
1212
13+newtype DataWithRequest = DataWithRequest (RequestF RequestBody ())
14+ deriving Show
15+
1316spec :: Spec
1417spec = do
1518 describe "Request" $ do
1619 describe "show" $ do
20+ it "has parenthesis correctly positioned" $ do
21+ let d = DataWithRequest (void defaultRequest)
22+ show d `shouldBe` "DataWithRequest (Request {requestPath = ()\
23+ \, requestQueryString = fromList []\
24+ \, requestBody = Nothing\
25+ \, requestAccept = fromList []\
26+ \, requestHeaders = fromList []\
27+ \, requestHttpVersion = HTTP/1.1\
28+ \, requestMethod = \"GET\"})"
1729 it "redacts the authorization header" $ do
1830 let request = void $ defaultRequest { requestHeaders = pure ("authorization", "secret") }
1931 isInfixOf "secret" (show request) `shouldBe` False
servant-client-ghcjs/servant-client-ghcjs.cabal+4−4
@@ -1,4 +1,4 @@
1−cabal-version: >=1.10
1+cabal-version: 2.2
22name: servant-client-ghcjs
33version: 0.16
44synopsis:
@@ -38,7 +38,7 @@ library
3838 Servant.Client.Internal.XhrClient
3939
4040 build-depends:
41− base >=4.11 && <4.12
41+ base >=4.11 && <5
4242 , bytestring >=0.10 && <0.12
4343 , case-insensitive >=1.2.0.0 && <1.3.0.0
4444 , containers >=0.5 && <0.7
@@ -56,8 +56,8 @@ library
5656
5757 -- strict, as we re-export stuff
5858 build-depends:
59− servant >=0.16 && <0.17
60− , servant-client-core >=0.16 && <0.16.1
59+ servant >=0.16 && <0.20
60+ , servant-client-core >=0.16 && <0.20
6161
6262 hs-source-dirs: src
6363 default-language: Haskell2010
servant-client-ghcjs/src/Servant/Client/Internal/XhrClient.hs+8−5
@@ -120,7 +120,7 @@ instance Exception StreamingNotSupportedException where
120120 displayException _ = "streamingRequest: streaming is not supported!"
121121
122122instance RunClient ClientM where
123− runRequest = performRequest
123+ runRequestAcceptStatus = performRequest
124124 throwClientError = throwError
125125
126126runClientMOrigin :: ClientM a -> ClientEnv -> IO (Either ClientError a)
@@ -152,15 +152,18 @@ runClientM m = do
152152
153153 runClientMOrigin m (ClientEnv (BaseUrl protocol hostname port ""))
154154
155−performRequest :: Request -> ClientM Response
156−performRequest req = do
155+performRequest :: Maybe [Status] -> Request -> ClientM Response
156+performRequest acceptStatus req = do
157157 xhr <- liftIO initXhr
158158 burl <- asks baseUrl
159159 liftIO $ performXhr xhr burl req
160160 resp <- toResponse xhr
161161
162− let status = statusCode (responseStatusCode resp)
163− unless (status >= 200 && status < 300) $ do
162+ let status = responseStatusCode resp
163+ goodStatus = case acceptStatus of
164+ Nothing -> statusIsSuccessful status
165+ Just good -> status `elem` good
166+ unless goodStatus $ do
164167 let f b = (burl, BL.toStrict $ toLazyByteString b)
165168 throwError $ FailureResponse (bimap (const ()) f req) resp
166169
servant-client/CHANGELOG.md+31
@@ -1,6 +1,37 @@
11[The latest version of this document is on GitHub.](https://github.com/haskell-servant/servant/blob/master/servant-client/CHANGELOG.md)
22[Changelog for `servant` package contains significant entries for all core packages.](https://github.com/haskell-servant/servant/blob/master/servant/CHANGELOG.md)
33
4+Package versions follow the [Package Versioning Policy](https://pvp.haskell.org/): in A.B.C, bumps to either A or B represent major versions.
5+
6+0.19
7+----
8+
9+### Significant changes
10+
11+- Drop support for GHC < 8.6.
12+- Support GHC 9.0 (GHC 9.2 should work as well, but isn't fully tested yet).
13+- Support Aeson 2 ([#1475](https://github.com/haskell-servant/servant/pull/1475)),
14+ which fixes a [DOS vulnerability](https://github.com/haskell/aeson/issues/864)
15+ related to hash collisions.
16+- Add `NamedRoutes` combinator, making support for records first-class in Servant
17+ ([#1388](https://github.com/haskell-servant/servant/pull/1388)).
18+- Add custom type errors for partially applied combinators
19+ ([#1289](https://github.com/haskell-servant/servant/pull/1289),
20+ [#1486](https://github.com/haskell-servant/servant/pull/1486)).
21+- *servant-client* / *servant-client-core* / *servant-http-streams*: Fix
22+ erroneous behavior, where only 2XX status codes would be considered
23+ successful, irrelevant of the status parameter specified by the verb
24+ combinator. ([#1469](https://github.com/haskell-servant/servant/pull/1469))
25+- *servant-client* / *servant-client-core*: Fix `Show` instance for
26+ `Servant.Client.Core.Request`.
27+- *servant-client* / *servant-client-core*: Allow passing arbitrary binary data
28+ in Query parameters.
29+ ([#1432](https://github.com/haskell-servant/servant/pull/1432)).
30+
31+### Other changes
32+
33+- Various version bumps.
34+
4350.18.3
536------
637
servant-client/servant-client.cabal+14−12
@@ -1,6 +1,6 @@
1−cabal-version: >=1.10
1+cabal-version: 2.2
22name: servant-client
3−version: 0.18.3
3+version: 0.19
44
55synopsis: Automatic derivation of querying functions for servant
66category: Servant, Web
@@ -41,14 +41,14 @@ library
4141 -- Bundled with GHC: Lower bound to not force re-installs
4242 -- text and mtl are bundled starting with GHC-8.4
4343 build-depends:
44− base >= 4.9 && < 4.16
44+ base >= 4.9 && < 4.17
4545 , bytestring >= 0.10.8.1 && < 0.12
4646 , containers >= 0.5.7.1 && < 0.7
4747 , deepseq >= 1.4.2.0 && < 1.5
4848 , mtl >= 2.2.2 && < 2.3
4949 , stm >= 2.4.5.1 && < 2.6
50− , text >= 1.2.3.0 && < 1.3
51− , time >= 1.6.0.1 && < 1.10
50+ , text >= 1.2.3.0 && < 2.1
51+ , time >= 1.6.0.1 && < 1.12
5252 , transformers >= 0.5.2.0 && < 0.6
5353
5454 if !impl(ghc >= 8.2)
@@ -58,13 +58,13 @@ library
5858 -- Servant dependencies.
5959 -- Strict dependency on `servant-client-core` as we re-export things.
6060 build-depends:
61− servant == 0.18.*
62− , servant-client-core >= 0.18.3 && <0.18.4
61+ servant >= 0.18 && < 0.20
62+ , servant-client-core >= 0.19 && < 0.19.1
6363
6464 -- Other dependencies: Lower bound around what is in the latest Stackage LTS.
6565 -- Here can be exceptions if we really need features from the newer versions.
6666 build-depends:
67− base-compat >= 0.10.5 && < 0.12
67+ base-compat >= 0.10.5 && < 0.13
6868 , http-client >= 0.5.13.1 && < 0.8
6969 , http-media >= 0.7.1.3 && < 0.9
7070 , http-types >= 0.12.2 && < 0.13
@@ -89,10 +89,12 @@ test-suite spec
8989 main-is: Spec.hs
9090 other-modules:
9191 Servant.BasicAuthSpec
92+ Servant.BrokenSpec
9293 Servant.ClientTestUtils
9394 Servant.ConnectionErrorSpec
9495 Servant.FailSpec
9596 Servant.GenAuthSpec
97+ Servant.GenericSpec
9698 Servant.HoistClientSpec
9799 Servant.StreamSpec
98100 Servant.SuccessSpec
@@ -122,16 +124,16 @@ test-suite spec
122124 -- Additional dependencies
123125 build-depends:
124126 entropy >= 0.4.1.3 && < 0.5
125− , hspec >= 2.6.0 && < 2.9
127+ , hspec >= 2.6.0 && < 2.10
126128 , HUnit >= 1.6.0.0 && < 1.7
127129 , network >= 2.8.0.0 && < 3.2
128130 , QuickCheck >= 2.12.6.1 && < 2.15
129− , servant == 0.18.*
130− , servant-server == 0.18.*
131+ , servant == 0.19.*
132+ , servant-server == 0.19.*
131133 , tdigest >= 0.2 && < 0.3
132134
133135 build-tool-depends:
134− hspec-discover:hspec-discover >= 2.6.0 && < 2.9
136+ hspec-discover:hspec-discover >= 2.6.0 && < 2.10
135137
136138test-suite readme
137139 type: exitcode-stdio-1.0
servant-client/src/Servant/Client/Internal/HttpClient.hs+11−11
@@ -24,7 +24,7 @@ import Control.Monad
2424import Control.Monad.Base
2525 (MonadBase (..))
2626import Control.Monad.Catch
27− (MonadCatch, MonadThrow)
27+ (MonadCatch, MonadThrow, MonadMask)
2828import Control.Monad.Error.Class
2929 (MonadError (..))
3030import Control.Monad.IO.Class
@@ -63,7 +63,7 @@ import GHC.Generics
6363import Network.HTTP.Media
6464 (renderHeader)
6565import Network.HTTP.Types
66− (hContentType, renderQuery, statusCode, urlEncode, Status)
66+ (hContentType, renderQuery, statusIsSuccessful, urlEncode, Status)
6767import Servant.Client.Core
6868
6969import qualified Network.HTTP.Client as Client
@@ -80,7 +80,7 @@ data ClientEnv
8080 { manager :: Client.Manager
8181 , baseUrl :: BaseUrl
8282 , cookieJar :: Maybe (TVar Client.CookieJar)
83− , makeClientRequest :: BaseUrl -> Request -> Client.Request
83+ , makeClientRequest :: BaseUrl -> Request -> IO Client.Request
8484 -- ^ this function can be used to customize the creation of @http-client@ requests from @servant@ requests. Default value: 'defaultMakeClientRequest'
8585 -- Note that:
8686 -- 1. 'makeClientRequest' exists to allow overriding operational semantics e.g. 'responseTimeout' per request,
@@ -136,7 +136,7 @@ newtype ClientM a = ClientM
136136 { unClientM :: ReaderT ClientEnv (ExceptT ClientError IO) a }
137137 deriving ( Functor, Applicative, Monad, MonadIO, Generic
138138 , MonadReader ClientEnv, MonadError ClientError, MonadThrow
139− , MonadCatch)
139+ , MonadCatch, MonadMask)
140140
141141instance MonadBase IO ClientM where
142142 liftBase = ClientM . liftBase
@@ -162,7 +162,7 @@ runClientM cm env = runExceptT $ flip runReaderT env $ unClientM cm
162162performRequest :: Maybe [Status] -> Request -> ClientM Response
163163performRequest acceptStatus req = do
164164 ClientEnv m burl cookieJar' createClientRequest <- ask
165− let clientRequest = createClientRequest burl req
165+ clientRequest <- liftIO $ createClientRequest burl req
166166 request <- case cookieJar' of
167167 Nothing -> pure clientRequest
168168 Just cj -> liftIO $ do
@@ -179,10 +179,9 @@ performRequest acceptStatus req = do
179179
180180 response <- maybe (requestWithoutCookieJar m request) (requestWithCookieJar m request) cookieJar'
181181 let status = Client.responseStatus response
182− status_code = statusCode status
183182 ourResponse = clientResponseToResponse id response
184183 goodStatus = case acceptStatus of
185− Nothing -> status_code >= 200 && status_code < 300
184+ Nothing -> statusIsSuccessful status
186185 Just good -> status `elem` good
187186 unless goodStatus $ do
188187 throwError $ mkFailureResponse burl req ourResponse
@@ -230,8 +229,8 @@ clientResponseToResponse f r = Response
230229-- | Create a @http-client@ 'Client.Request' from a @servant@ 'Request'
231230-- The 'Client.host', 'Client.path' and 'Client.port' fields are extracted from the 'BaseUrl'
232231-- otherwise the body, headers and query string are derived from the @servant@ 'Request'
233−defaultMakeClientRequest :: BaseUrl -> Request -> Client.Request
234−defaultMakeClientRequest burl r = Client.defaultRequest
232+defaultMakeClientRequest :: BaseUrl -> Request -> IO Client.Request
233+defaultMakeClientRequest burl r = return Client.defaultRequest
235234 { Client.method = requestMethod r
236235 , Client.host = fromString $ baseUrlHost burl
237236 , Client.port = baseUrlPort burl
@@ -247,7 +246,7 @@ defaultMakeClientRequest burl r = Client.defaultRequest
247246 where
248247 -- Content-Type and Accept are specified by requestBody and requestAccept
249248 headers = filter (\(h, _) -> h /= "Accept" && h /= "Content-Type") $
250− toList $requestHeaders r
249+ toList $ requestHeaders r
251250
252251 acceptHdr
253252 | null hs = Nothing
@@ -290,7 +289,8 @@ defaultMakeClientRequest burl r = Client.defaultRequest
290289 Https -> True
291290
292291 -- Query string builder which does not do any encoding
293− buildQueryString = ("?" <>) . foldl' addQueryParam mempty
292+ buildQueryString [] = mempty
293+ buildQueryString qps = "?" <> foldl' addQueryParam mempty qps
294294
295295 addQueryParam qs (k, v) =
296296 qs <> (if BS.null qs then mempty else "&") <> urlEncode True k <> foldMap ("=" <>) v
servant-client/src/Servant/Client/Internal/HttpClient/Streaming.hs+5−7
@@ -47,7 +47,7 @@ import Data.Time.Clock
4747 (getCurrentTime)
4848import GHC.Generics
4949import Network.HTTP.Types
50− (Status, statusCode)
50+ (Status, statusIsSuccessful)
5151
5252import qualified Network.HTTP.Client as Client
5353
@@ -140,7 +140,7 @@ performRequest :: Maybe [Status] -> Request -> ClientM Response
140140performRequest acceptStatus req = do
141141 -- TODO: should use Client.withResponse here too
142142 ClientEnv m burl cookieJar' createClientRequest <- ask
143− let clientRequest = createClientRequest burl req
143+ clientRequest <- liftIO $ createClientRequest burl req
144144 request <- case cookieJar' of
145145 Nothing -> pure clientRequest
146146 Just cj -> liftIO $ do
@@ -163,10 +163,9 @@ performRequest acceptStatus req = do
163163 now' <- getCurrentTime
164164 atomically $ modifyTVar' cj (fst . Client.updateCookieJar response request now')
165165 let status = Client.responseStatus response
166− status_code = statusCode status
167166 ourResponse = clientResponseToResponse id response
168167 goodStatus = case acceptStatus of
169− Nothing -> status_code >= 200 && status_code < 300
168+ Nothing -> statusIsSuccessful status
170169 Just good -> status `elem` good
171170 unless goodStatus $ do
172171 throwError $ mkFailureResponse burl req ourResponse
@@ -178,14 +177,13 @@ performWithStreamingRequest req k = do
178177 m <- asks manager
179178 burl <- asks baseUrl
180179 createClientRequest <- asks makeClientRequest
181− let request = createClientRequest burl req
180+ request <- liftIO $ createClientRequest burl req
182181 ClientM $ lift $ lift $ Codensity $ \k1 ->
183182 Client.withResponse request m $ \res -> do
184183 let status = Client.responseStatus res
185− status_code = statusCode status
186184
187185 -- we throw FailureResponse in IO :(
188− unless (status_code >= 200 && status_code < 300) $ do
186+ unless (statusIsSuccessful status) $ do
189187 b <- BSL.fromChunks <$> Client.brConsume (Client.responseBody res)
190188 throwIO $ mkFailureResponse burl req (clientResponseToResponse (const b) res)
191189
servant-client/test/Servant/BrokenSpec.hsnew file+71
@@ -0,0 +1,71 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE TypeOperators #-}
3+{-# OPTIONS_GHC -freduction-depth=100 #-}
4+{-# OPTIONS_GHC -fno-warn-orphans #-}
5+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
6+
7+module Servant.BrokenSpec (spec) where
8+
9+import Prelude ()
10+import Prelude.Compat
11+
12+import Data.Monoid ()
13+import Data.Proxy
14+import qualified Network.HTTP.Types as HTTP
15+import Test.Hspec
16+
17+import Servant.API
18+ ((:<|>) ((:<|>)), (:>), JSON, Verb, Get, StdMethod (GET))
19+import Servant.Client
20+import Servant.ClientTestUtils
21+import Servant.Server
22+
23+-- * api for testing inconsistencies between client and server
24+
25+type Get201 = Verb 'GET 201
26+type Get301 = Verb 'GET 301
27+
28+type BrokenAPI =
29+ -- the server should respond with 200, but returns 201
30+ "get200" :> Get201 '[JSON] ()
31+ -- the server should respond with 307, but returns 301
32+ :<|> "get307" :> Get301 '[JSON] ()
33+
34+brokenApi :: Proxy BrokenAPI
35+brokenApi = Proxy
36+
37+brokenServer :: Application
38+brokenServer = serve brokenApi (pure () :<|> pure ())
39+
40+type PublicAPI =
41+ -- the client expects 200
42+ "get200" :> Get '[JSON] ()
43+ -- the client expects 307
44+ :<|> "get307" :> Get307 '[JSON] ()
45+
46+publicApi :: Proxy PublicAPI
47+publicApi = Proxy
48+
49+get200Client :: ClientM ()
50+get307Client :: ClientM ()
51+get200Client :<|> get307Client = client publicApi
52+
53+
54+spec :: Spec
55+spec = describe "Servant.BrokenSpec" $ do
56+ brokenSpec
57+
58+brokenSpec :: Spec
59+brokenSpec = beforeAll (startWaiApp brokenServer) $ afterAll endWaiApp $ do
60+ context "client returns errors for inconsistencies between client and server api" $ do
61+ it "reports FailureResponse with wrong 2xx status code" $ \(_, baseUrl) -> do
62+ res <- runClient get200Client baseUrl
63+ case res of
64+ Left (FailureResponse _ r) | responseStatusCode r == HTTP.status201 -> return ()
65+ _ -> fail $ "expected 201 broken response, but got " <> show res
66+
67+ it "reports FailureResponse with wrong 3xx status code" $ \(_, baseUrl) -> do
68+ res <- runClient get307Client baseUrl
69+ case res of
70+ Left (FailureResponse _ r) | responseStatusCode r == HTTP.status301 -> return ()
71+ _ -> fail $ "expected 301 broken response, but got " <> show res
servant-client/test/Servant/ClientTestUtils.hs+48−4
@@ -64,7 +64,8 @@ import Servant.API
6464 JSON, MimeRender (mimeRender), MimeUnrender (mimeUnrender),
6565 NoContent (NoContent), PlainText, Post, QueryFlag, QueryParam,
6666 QueryParams, Raw, ReqBody, StdMethod (GET), ToHttpApiData (..), UVerb, Union,
67− WithStatus (WithStatus), addHeader)
67+ Verb, WithStatus (WithStatus), NamedRoutes, addHeader)
68+import Servant.API.Generic ((:-))
6869import Servant.Client
6970import qualified Servant.Client.Core.Auth as Auth
7071import Servant.Server
@@ -107,9 +108,26 @@ carol = Person "Carol" 17
107108
108109type TestHeaders = '[Header "X-Example1" Int, Header "X-Example2" String]
109110
111+data RecordRoutes mode = RecordRoutes
112+ { version :: mode :- "version" :> Get '[JSON] Int
113+ , echo :: mode :- "echo" :> Capture "string" String :> Get '[JSON] String
114+ , otherRoutes :: mode :- "other" :> Capture "someParam" Int :> NamedRoutes OtherRoutes
115+ } deriving Generic
116+
117+data OtherRoutes mode = OtherRoutes
118+ { something :: mode :- "something" :> Get '[JSON] [String]
119+ } deriving Generic
120+
121+-- Get for HTTP 307 Temporary Redirect
122+type Get307 = Verb 'GET 307
123+
110124type Api =
111125 Get '[JSON] Person
112126 :<|> "get" :> Get '[JSON] Person
127+ -- This endpoint returns a response with status code 307 Temporary Redirect,
128+ -- different from the ones in the 2xx successful class, to test derivation
129+ -- of clients' api.
130+ :<|> "get307" :> Get307 '[PlainText] Text
113131 :<|> "deleteEmpty" :> DeleteNoContent
114132 :<|> "capture" :> Capture "name" String :> Get '[JSON,FormUrlEncoded] Person
115133 :<|> "captureAll" :> CaptureAll "names" String :> Get '[JSON] [Person]
@@ -141,13 +159,15 @@ type Api =
141159 UVerb 'GET '[PlainText] '[WithStatus 200 Person,
142160 WithStatus 301 Text]
143161 :<|> "uverb-get-created" :> UVerb 'GET '[PlainText] '[WithStatus 201 Person]
144−
162+ :<|> NamedRoutes RecordRoutes
163+ :<|> "captureVerbatim" :> Capture "someString" Verbatim :> Get '[PlainText] Text
145164
146165api :: Proxy Api
147166api = Proxy
148167
149168getRoot :: ClientM Person
150169getGet :: ClientM Person
170+getGet307 :: ClientM Text
151171getDeleteEmpty :: ClientM NoContent
152172getCapture :: String -> ClientM Person
153173getCaptureAll :: [String] -> ClientM [Person]
@@ -170,9 +190,11 @@ uverbGetSuccessOrRedirect :: Bool
170190 -> ClientM (Union '[WithStatus 200 Person,
171191 WithStatus 301 Text])
172192uverbGetCreated :: ClientM (Union '[WithStatus 201 Person])
193+recordRoutes :: RecordRoutes (AsClientT ClientM)
173194
174195getRoot
175196 :<|> getGet
197+ :<|> getGet307
176198 :<|> getDeleteEmpty
177199 :<|> getCapture
178200 :<|> getCaptureAll
@@ -192,12 +214,15 @@ getRoot
192214 :<|> getRedirectWithCookie
193215 :<|> EmptyClient
194216 :<|> uverbGetSuccessOrRedirect
195− :<|> uverbGetCreated = client api
217+ :<|> uverbGetCreated
218+ :<|> recordRoutes
219+ :<|> captureVerbatim = client api
196220
197221server :: Application
198222server = serve api (
199223 return carol
200224 :<|> return alice
225+ :<|> return "redirecting"
201226 :<|> return NoContent
202227 :<|> (\ name -> return $ Person name 0)
203228 :<|> (\ names -> return (zipWith Person names [0..]))
@@ -229,8 +254,18 @@ server = serve api (
229254 then respond (WithStatus @301 ("redirecting" :: Text))
230255 else respond (WithStatus @200 alice ))
231256 :<|> respond (WithStatus @201 carol)
257+ :<|> RecordRoutes
258+ { version = pure 42
259+ , echo = pure
260+ , otherRoutes = \_ -> OtherRoutes
261+ { something = pure ["foo", "bar", "pweet"]
262+ }
263+ }
264+ :<|> pure . decodeUtf8 . unVerbatim
232265 )
233266
267+-- * api for testing failures
268+
234269type FailApi =
235270 "get" :> Raw
236271 :<|> "capture" :> Capture "name" String :> Raw
@@ -245,7 +280,7 @@ failServer = serve failApi (
245280 :<|> (\ _capture -> Tagged $ \_request respond -> respond $ Wai.responseLBS HTTP.ok200 [("content-type", "application/json")] "")
246281 :<|> (Tagged $ \_request respond -> respond $ Wai.responseLBS HTTP.ok200 [("content-type", "fooooo")] "")
247282 :<|> (Tagged $ \_request respond -> respond $ Wai.responseLBS HTTP.ok200 [("content-type", "application/x-www-form-urlencoded"), ("X-Example1", "1"), ("X-Example2", "foo")] "")
248− )
283+ )
249284
250285-- * basic auth stuff
251286
@@ -338,3 +373,12 @@ instance ToHttpApiData UrlEncodedByteString where
338373
339374instance FromHttpApiData UrlEncodedByteString where
340375 parseUrlPiece = pure . UrlEncodedByteString . HTTP.urlDecode True . encodeUtf8
376+
377+newtype Verbatim = Verbatim { unVerbatim :: ByteString }
378+
379+instance ToHttpApiData Verbatim where
380+ toEncodedUrlPiece = byteString . unVerbatim
381+ toUrlPiece = decodeUtf8 . unVerbatim
382+
383+instance FromHttpApiData Verbatim where
384+ parseUrlPiece = pure . Verbatim . encodeUtf8
servant-client/test/Servant/FailSpec.hs+3−3
@@ -38,14 +38,14 @@ failSpec = beforeAll (startWaiApp failServer) $ afterAll endWaiApp $ do
3838
3939 context "client returns errors appropriately" $ do
4040 it "reports FailureResponse" $ \(_, baseUrl) -> do
41− let (_ :<|> _ :<|> getDeleteEmpty :<|> _) = client api
41+ let (_ :<|> _ :<|> _ :<|> getDeleteEmpty :<|> _) = client api
4242 Left res <- runClient getDeleteEmpty baseUrl
4343 case res of
4444 FailureResponse _ r | responseStatusCode r == HTTP.status404 -> return ()
4545 _ -> fail $ "expected 404 response, but got " <> show res
4646
4747 it "reports DecodeFailure" $ \(_, baseUrl) -> do
48− let (_ :<|> _ :<|> _ :<|> getCapture :<|> _) = client api
48+ let (_ :<|> _ :<|> _ :<|> _ :<|> getCapture :<|> _) = client api
4949 Left res <- runClient (getCapture "foo") baseUrl
5050 case res of
5151 DecodeFailure _ _ -> return ()
@@ -72,7 +72,7 @@ failSpec = beforeAll (startWaiApp failServer) $ afterAll endWaiApp $ do
7272 _ -> fail $ "expected UnsupportedContentType, but got " <> show res
7373
7474 it "reports InvalidContentTypeHeader" $ \(_, baseUrl) -> do
75− let (_ :<|> _ :<|> _ :<|> _ :<|> _ :<|> getBody :<|> _) = client api
75+ let (_ :<|> _ :<|> _ :<|> _ :<|> _ :<|> _ :<|> getBody :<|> _) = client api
7676 Left res <- runClient (getBody alice) baseUrl
7777 case res of
7878 InvalidContentTypeHeader _ -> return ()
servant-client/test/Servant/GenericSpec.hsnew file+37
@@ -0,0 +1,37 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE ConstraintKinds #-}
3+{-# LANGUAGE DataKinds #-}
4+{-# LANGUAGE FlexibleContexts #-}
5+{-# LANGUAGE FlexibleInstances #-}
6+{-# LANGUAGE GADTs #-}
7+{-# LANGUAGE MultiParamTypeClasses #-}
8+{-# LANGUAGE OverloadedStrings #-}
9+{-# LANGUAGE PolyKinds #-}
10+{-# LANGUAGE ScopedTypeVariables #-}
11+{-# LANGUAGE TypeFamilies #-}
12+{-# LANGUAGE TypeOperators #-}
13+{-# LANGUAGE UndecidableInstances #-}
14+{-# OPTIONS_GHC -freduction-depth=100 #-}
15+{-# OPTIONS_GHC -fno-warn-orphans #-}
16+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
17+
18+module Servant.GenericSpec (spec) where
19+
20+import Test.Hspec
21+
22+import Servant.Client ((//), (/:))
23+import Servant.ClientTestUtils
24+
25+spec :: Spec
26+spec = describe "Servant.GenericSpec" $ do
27+ genericSpec
28+
29+genericSpec :: Spec
30+genericSpec = beforeAll (startWaiApp server) $ afterAll endWaiApp $ do
31+ context "Record clients work as expected" $ do
32+
33+ it "Client functions return expected values" $ \(_,baseUrl) -> do
34+ runClient (recordRoutes // version) baseUrl `shouldReturn` Right 42
35+ runClient (recordRoutes // echo /: "foo") baseUrl `shouldReturn` Right "foo"
36+ it "Clients can be nested" $ \(_,baseUrl) -> do
37+ runClient (recordRoutes // otherRoutes /: 42 // something) baseUrl `shouldReturn` Right ["foo", "bar", "pweet"]
servant-client/test/Servant/SuccessSpec.hs+21−6
@@ -36,6 +36,8 @@ import Data.Maybe
3636import Data.Monoid ()
3737import Data.Text
3838 (Text)
39+import Data.Text.Encoding
40+ (encodeUtf8)
3941import qualified Network.HTTP.Client as C
4042import qualified Network.HTTP.Types as HTTP
4143import Test.Hspec
@@ -59,11 +61,15 @@ spec = describe "Servant.SuccessSpec" $ do
5961
6062successSpec :: Spec
6163successSpec = beforeAll (startWaiApp server) $ afterAll endWaiApp $ do
62− it "Servant.API.Get root" $ \(_, baseUrl) -> do
63− left show <$> runClient getRoot baseUrl `shouldReturn` Right carol
64+ describe "Servant.API.Get" $ do
65+ it "get root endpoint" $ \(_, baseUrl) -> do
66+ left show <$> runClient getRoot baseUrl `shouldReturn` Right carol
6467
65− it "Servant.API.Get" $ \(_, baseUrl) -> do
66− left show <$> runClient getGet baseUrl `shouldReturn` Right alice
68+ it "get simple endpoint" $ \(_, baseUrl) -> do
69+ left show <$> runClient getGet baseUrl `shouldReturn` Right alice
70+
71+ it "get redirection endpoint" $ \(_, baseUrl) -> do
72+ left show <$> runClient getGet307 baseUrl `shouldReturn` Right "redirecting"
6773
6874 describe "Servant.API.Delete" $ do
6975 it "allows empty content type" $ \(_, baseUrl) -> do
@@ -111,6 +117,7 @@ successSpec = beforeAll (startWaiApp server) $ afterAll endWaiApp $ do
111117
112118 it "Servant.API.Fragment" $ \(_, baseUrl) -> do
113119 left id <$> runClient getFragment baseUrl `shouldReturn` Right alice
120+
114121 it "Servant.API.Raw on success" $ \(_, baseUrl) -> do
115122 res <- runClient (getRawSuccess HTTP.methodGet) baseUrl
116123 case res of
@@ -155,8 +162,9 @@ successSpec = beforeAll (startWaiApp server) $ afterAll endWaiApp $ do
155162 mgr <- C.newManager C.defaultManagerSettings
156163 -- In proper situation, extra headers should probably be visible in API type.
157164 -- However, testing for response timeout is difficult, so we test with something which is easy to observe
158− let createClientRequest url r = (defaultMakeClientRequest url r) { C.requestHeaders = [("X-Added-Header", "XXX")] }
159− let clientEnv = (mkClientEnv mgr baseUrl) { makeClientRequest = createClientRequest }
165+ let createClientRequest url r = fmap (\req -> req { C.requestHeaders = [("X-Added-Header", "XXX")] })
166+ (defaultMakeClientRequest url r)
167+ clientEnv = (mkClientEnv mgr baseUrl) { makeClientRequest = createClientRequest }
160168 res <- runClientM (getRawSuccessPassHeaders HTTP.methodGet) clientEnv
161169 case res of
162170 Left e ->
@@ -191,3 +199,10 @@ successSpec = beforeAll (startWaiApp server) $ afterAll endWaiApp $ do
191199 case eitherResponse of
192200 Left clientError -> fail $ show clientError
193201 Right response -> matchUnion response `shouldBe` Just (WithStatus @201 carol)
202+
203+ it "encodes URL pieces following ToHttpApiData instance" $ \(_, baseUrl) -> do
204+ let textOrig = "*"
205+ eitherResponse <- runClient (captureVerbatim $ Verbatim $ encodeUtf8 textOrig) baseUrl
206+ case eitherResponse of
207+ Left clientError -> fail $ show clientError
208+ Right textBack -> textBack `shouldBe` textOrig
servant-conduit/servant-conduit.cabal+5−5
@@ -1,4 +1,4 @@
1−cabal-version: >=1.10
1+cabal-version: 2.2
22name: servant-conduit
33version: 0.15.1
44
@@ -23,7 +23,7 @@ extra-source-files:
2323
2424source-repository head
2525 type: git
26− location: http://github.com/haskell-servant/servant-conduit.git
26+ location: http://github.com/haskell-servant/servant.git
2727
2828library
2929 exposed-modules: Servant.Conduit
@@ -33,7 +33,7 @@ library
3333 , conduit >=1.3.1 && <1.4
3434 , mtl >=2.2.2 && <2.3
3535 , resourcet >=1.2.2 && <1.3
36− , servant >=0.15 && <0.19
36+ , servant >=0.15 && <0.20
3737 , unliftio-core >=0.1.2.0 && <0.3
3838 hs-source-dirs: src
3939 default-language: Haskell2010
@@ -54,8 +54,8 @@ test-suite example
5454 , resourcet
5555 , servant
5656 , servant-conduit
57− , servant-server >=0.15 && <0.19
58− , servant-client >=0.15 && <0.19
57+ , servant-server >=0.15 && <0.20
58+ , servant-client >=0.15 && <0.20
5959 , wai >=3.2.1.2 && <3.3
6060 , warp >=3.2.25 && <3.4
6161 , http-client
servant-docs/CHANGELOG.md+9
@@ -1,6 +1,15 @@
11[The latest version of this document is on GitHub.](https://github.com/haskell-servant/servant/blob/master/servant-docs/CHANGELOG.md)
22[Changelog for `servant` package contains significant entries for all core packages.](https://github.com/haskell-servant/servant/blob/master/servant/CHANGELOG.md)
33
4+0.12
5+----
6+
7+### Significant changes
8+
9+- Generate sample cURL requests
10+ ([#1401](https://github.com/haskell-servant/servant/pull/1401/files)).
11+ Breaking change: requires sample header values to be supplied with `headers`.
12+
4130.11.9
514------
615
servant-docs/servant-docs.cabal+9−9
@@ -1,6 +1,6 @@
1−cabal-version: >=1.10
1+cabal-version: 2.2
22name: servant-docs
3−version: 0.11.9
3+version: 0.12
44
55synopsis: generate API docs for your servant webservice
66category: Servant, Web
@@ -41,25 +41,25 @@ library
4141 --
4242 -- note: mtl lower bound is so low because of GHC-7.8
4343 build-depends:
44− base >= 4.9 && < 4.16
44+ base >= 4.9 && < 4.17
4545 , bytestring >= 0.10.8.1 && < 0.12
46− , text >= 1.2.3.0 && < 1.3
46+ , text >= 1.2.3.0 && < 2.1
4747
4848 -- Servant dependencies
4949 build-depends:
50− servant >= 0.18 && <0.19
50+ servant >= 0.18 && <0.20
5151
5252 -- Other dependencies: Lower bound around what is in the latest Stackage LTS.
5353 -- Here can be exceptions if we really need features from the newer versions.
5454 build-depends:
55− aeson >= 1.4.1.0 && < 1.6
55+ aeson >= 1.4.1.0 && < 3
5656 , aeson-pretty >= 0.8.5 && < 0.9
57− , base-compat >= 0.10.5 && < 0.12
57+ , base-compat >= 0.10.5 && < 0.13
5858 , case-insensitive >= 1.2.0.11 && < 1.3
59− , hashable >= 1.2.7.0 && < 1.4
59+ , hashable >= 1.2.7.0 && < 1.5
6060 , http-media >= 0.7.1.3 && < 0.9
6161 , http-types >= 0.12.2 && < 0.13
62− , lens >= 4.17 && < 5.1
62+ , lens >= 4.17 && < 5.2
6363 , string-conversions >= 0.4.0.1 && < 0.5
6464 , universe-base >= 1.1.1 && < 1.2
6565 , unordered-containers >= 0.2.9.0 && < 0.3
servant-docs/src/Servant/Docs/Internal.hs+4
@@ -62,6 +62,7 @@ import GHC.TypeLits
6262import Servant.API
6363import Servant.API.ContentTypes
6464import Servant.API.TypeLevel
65+import Servant.API.Generic
6566
6667import qualified Data.Universe.Helpers as U
6768
@@ -1150,6 +1151,9 @@ instance (ToAuthInfo (BasicAuth realm usr), HasDocs api) => HasDocs (BasicAuth r
11501151 authProxy = Proxy :: Proxy (BasicAuth realm usr)
11511152 action' = over authInfo (|> toAuthInfo authProxy) action
11521153
1154+instance HasDocs (ToServantApi api) => HasDocs (NamedRoutes api) where
1155+ docsFor Proxy = docsFor (Proxy :: Proxy (ToServantApi api))
1156+
11531157-- ToSample instances for simple types
11541158instance ToSample NoContent
11551159instance ToSample Bool
servant-foreign/servant-foreign.cabal+8−8
@@ -1,4 +1,4 @@
1−cabal-version: >=1.10
1+cabal-version: 2.2
22name: servant-foreign
33version: 0.15.4
44
@@ -41,18 +41,18 @@ library
4141 --
4242 -- note: mtl lower bound is so low because of GHC-7.8
4343 build-depends:
44− base >= 4.9 && < 4.16
45− , text >= 1.2.3.0 && < 1.3
44+ base >= 4.9 && < 4.17
45+ , text >= 1.2.3.0 && < 2.1
4646
4747 -- Servant dependencies
4848 build-depends:
49− servant >=0.18 && <0.19
49+ servant >=0.18 && <0.20
5050
5151 -- Other dependencies: Lower bound around what is in the latest Stackage LTS.
5252 -- Here can be exceptions if we really need features from the newer versions.
5353 build-depends:
54− base-compat >= 0.10.5 && < 0.12
55− , lens >= 4.17 && < 5.1
54+ base-compat >= 0.10.5 && < 0.13
55+ , lens >= 4.17 && < 5.2
5656 , http-types >= 0.12.2 && < 0.13
5757
5858 hs-source-dirs: src
@@ -74,7 +74,7 @@ test-suite spec
7474
7575 -- Additional dependencies
7676 build-depends:
77− hspec >= 2.6.0 && <2.9
77+ hspec >= 2.6.0 && <2.10
7878 build-tool-depends:
79− hspec-discover:hspec-discover >=2.6.0 && <2.9
79+ hspec-discover:hspec-discover >=2.6.0 && <2.10
8080 default-language: Haskell2010
servant-http-streams/CHANGELOG.md+10
@@ -1,6 +1,16 @@
11 [The latest version of this document is on GitHub.](https://github.com/haskell-servant/servant/blob/master/servant-http-streams/CHANGELOG.md)
22[Changelog for `servant` package contains significant entries for all core packages.](https://github.com/haskell-servant/servant/blob/master/servant/CHANGELOG.md)
33
4+0.18.4
5+------
6+
7+### Significant changes
8+
9+- *servant-client* / *servant-client-core* / *servant-http-streams*:
10+ Fix erroneous behavior, where only 2XX status codes would be considered
11+ successful, irrelevant of the status parameter specified by the verb
12+ combinator. ([#1469](https://github.com/haskell-servant/servant/pull/1469))
13+
4140.18.3
515------
616
servant-http-streams/servant-http-streams.cabal+14−14
@@ -1,6 +1,6 @@
1−cabal-version: >=1.10
1+cabal-version: 2.2
22name: servant-http-streams
3−version: 0.18.3
3+version: 0.18.4
44
55synopsis: Automatic derivation of querying functions for servant
66category: Servant, Web
@@ -38,13 +38,13 @@ library
3838 -- Bundled with GHC: Lower bound to not force re-installs
3939 -- text and mtl are bundled starting with GHC-8.4
4040 build-depends:
41− base >= 4.9 && < 4.16
41+ base >= 4.9 && < 4.17
4242 , bytestring >= 0.10.8.1 && < 0.12
4343 , containers >= 0.5.7.1 && < 0.7
4444 , deepseq >= 1.4.2.0 && < 1.5
4545 , mtl >= 2.2.2 && < 2.3
46− , text >= 1.2.3.0 && < 1.3
47− , time >= 1.6.0.1 && < 1.10
46+ , text >= 1.2.3.0 && < 2.1
47+ , time >= 1.6.0.1 && < 1.12
4848 , transformers >= 0.5.2.0 && < 0.6
4949
5050 if !impl(ghc >= 8.2)
@@ -54,25 +54,25 @@ library
5454 -- Servant dependencies.
5555 -- Strict dependency on `servant-client-core` as we re-export things.
5656 build-depends:
57− servant == 0.18.*
58− , servant-client-core >= 0.18.3 && <0.18.4
57+ servant >= 0.18 && < 0.20
58+ , servant-client-core >= 0.18.3 && <0.20
5959
6060 -- Other dependencies: Lower bound around what is in the latest Stackage LTS.
6161 -- Here can be exceptions if we really need features from the newer versions.
6262 build-depends:
63− base-compat >= 0.10.5 && < 0.12
63+ base-compat >= 0.10.5 && < 0.13
6464 , case-insensitive
6565 , http-streams >= 0.8.6.1 && < 0.9
6666 , http-media >= 0.7.1.3 && < 0.9
6767 , io-streams >= 1.5.0.1 && < 1.6
6868 , http-types >= 0.12.2 && < 0.13
69− , http-common >= 0.8.2.0 && < 0.8.3
69+ , http-common >= 0.8.2.0 && < 0.9
7070 , exceptions >= 0.10.0 && < 0.11
7171 , kan-extensions >= 5.2 && < 5.3
7272 , monad-control >= 1.0.2.3 && < 1.1
7373 , semigroupoids >= 5.3.1 && < 5.4
7474 , transformers-base >= 0.4.5.2 && < 0.5
75− , transformers-compat >= 0.6.2 && < 0.7
75+ , transformers-compat >= 0.6.2 && < 0.8
7676
7777 hs-source-dirs: src
7878 default-language: Haskell2010
@@ -112,16 +112,16 @@ test-suite spec
112112 -- Additional dependencies
113113 build-depends:
114114 entropy >= 0.4.1.3 && < 0.5
115− , hspec >= 2.6.0 && < 2.9
115+ , hspec >= 2.6.0 && < 2.10
116116 , HUnit >= 1.6.0.0 && < 1.7
117117 , network >= 2.8.0.0 && < 3.2
118118 , QuickCheck >= 2.12.6.1 && < 2.15
119− , servant == 0.18.*
120− , servant-server == 0.18.*
119+ , servant == 0.19.*
120+ , servant-server == 0.19.*
121121 , tdigest >= 0.2 && < 0.3
122122
123123 build-tool-depends:
124− hspec-discover:hspec-discover >= 2.6.0 && < 2.9
124+ hspec-discover:hspec-discover >= 2.6.0 && < 2.10
125125
126126test-suite readme
127127 type: exitcode-stdio-1.0
servant-http-streams/src/Servant/HttpStreams/Internal.hs+6−6
@@ -57,7 +57,7 @@ import GHC.Generics
5757import Network.HTTP.Media
5858 (renderHeader)
5959import Network.HTTP.Types
60− (Status (..), hContentType, http11, renderQuery)
60+ (Status (..), hContentType, http11, renderQuery, statusIsSuccessful)
6161import Servant.Client.Core
6262
6363import qualified Network.Http.Client as Client
@@ -160,12 +160,12 @@ performRequest acceptStatus req = do
160160 x <- ClientM $ lift $ lift $ Codensity $ \k -> do
161161 Client.sendRequest conn req' body
162162 Client.receiveResponse conn $ \res' body' -> do
163− let sc = Client.getStatusCode res'
163+ let status = toEnum $ Client.getStatusCode res'
164164 lbs <- BSL.fromChunks <$> Streams.toList body'
165165 let res'' = clientResponseToResponse res' lbs
166166 goodStatus = case acceptStatus of
167− Nothing -> sc >= 200 && sc < 300
168− Just good -> sc `elem` (statusCode <$> good)
167+ Nothing -> statusIsSuccessful status
168+ Just good -> status `elem` good
169169 if goodStatus
170170 then k (Right res'')
171171 else k (Left (mkFailureResponse burl req res''))
@@ -180,8 +180,8 @@ performWithStreamingRequest req k = do
180180 Client.sendRequest conn req' body
181181 Client.receiveResponseRaw conn $ \res' body' -> do
182182 -- check status code
183− let sc = Client.getStatusCode res'
184− unless (sc >= 200 && sc < 300) $ do
183+ let status = toEnum $ Client.getStatusCode res'
184+ unless (statusIsSuccessful status) $ do
185185 lbs <- BSL.fromChunks <$> Streams.toList body'
186186 throwIO $ mkFailureResponse burl req (clientResponseToResponse res' lbs)
187187
servant-machines/servant-machines.cabal+5−5
@@ -1,4 +1,4 @@
1−cabal-version: >=1.10
1+cabal-version: 2.2
22name: servant-machines
33version: 0.15.1
44
@@ -23,7 +23,7 @@ extra-source-files:
2323
2424source-repository head
2525 type: git
26− location: http://github.com/haskell-servant/servant-machines.git
26+ location: http://github.com/haskell-servant/servant.git
2727
2828library
2929 exposed-modules: Servant.Machines
@@ -32,7 +32,7 @@ library
3232 , bytestring >=0.10.8.1 && <0.12
3333 , machines >=0.6.4 && <0.8
3434 , mtl >=2.2.2 && <2.3
35− , servant >=0.15 && <0.19
35+ , servant >=0.15 && <0.20
3636 hs-source-dirs: src
3737 default-language: Haskell2010
3838 ghc-options: -Wall
@@ -51,8 +51,8 @@ test-suite example
5151 , servant
5252 , machines
5353 , servant-machines
54− , servant-server >=0.15 && <0.19
55− , servant-client >=0.15 && <0.19
54+ , servant-server >=0.15 && <0.20
55+ , servant-client >=0.15 && <0.20
5656 , wai >=3.2.1.2 && <3.3
5757 , warp >=3.2.25 && <3.4
5858 , http-client
servant-pipes/servant-pipes.cabal+6−6
@@ -1,4 +1,4 @@
1−cabal-version: >=1.10
1+cabal-version: 2.2
22name: servant-pipes
33version: 0.15.3
44
@@ -23,7 +23,7 @@ extra-source-files:
2323
2424source-repository head
2525 type: git
26− location: http://github.com/haskell-servant/servant-pipes.git
26+ location: http://github.com/haskell-servant/servant.git
2727
2828library
2929 exposed-modules: Servant.Pipes
@@ -34,7 +34,7 @@ library
3434 , pipes-safe >=2.3.1 && <2.4
3535 , mtl >=2.2.2 && <2.3
3636 , monad-control >=1.0.2.3 && <1.1
37− , servant >=0.15 && <0.19
37+ , servant >=0.15 && <0.20
3838 hs-source-dirs: src
3939 default-language: Haskell2010
4040 ghc-options: -Wall
@@ -51,12 +51,12 @@ test-suite example
5151 , bytestring
5252 , http-media
5353 , servant
54− , pipes
54+ , pipes
5555 , pipes-safe
5656 , servant-pipes
5757 , pipes-bytestring >=2.1.6 && <2.2
58− , servant-server >=0.15 && <0.19
59− , servant-client >=0.15 && <0.19
58+ , servant-server >=0.15 && <0.20
59+ , servant-client >=0.15 && <0.20
6060 , wai >=3.2.1.2 && <3.3
6161 , warp >=3.2.25 && <3.4
6262 , http-client
servant-server/CHANGELOG.md+24
@@ -1,6 +1,30 @@
11[The latest version of this document is on GitHub.](https://github.com/haskell-servant/servant/blob/master/servant-server/CHANGELOG.md)
22[Changelog for `servant` package contains significant entries for all core packages.](https://github.com/haskell-servant/servant/blob/master/servant/CHANGELOG.md)
33
4+Package versions follow the [Package Versioning Policy](https://pvp.haskell.org/): in A.B.C, bumps to either A or B represent major versions.
5+
6+0.19.1
7+------
8+
9+- Add `MonadFail` instance for `Handler` wrt [#1545](https://github.com/haskell-servant/servant/issues/1545)
10+- Support GHC 9.2 [#1525](https://github.com/haskell-servant/servant/issues/1525)
11+
12+0.19
13+----
14+
15+### Significant changes
16+
17+- Drop support for GHC < 8.6.
18+- Support GHC 9.0 (GHC 9.2 should work as well, but isn't fully tested yet).
19+- Support Aeson 2 ([#1475](https://github.com/haskell-servant/servant/pull/1475)),
20+ which fixes a [DOS vulnerability](https://github.com/haskell/aeson/issues/864)
21+ related to hash collisions.
22+- Add `NamedRoutes` combinator, making support for records first-class in Servant
23+ ([#1388](https://github.com/haskell-servant/servant/pull/1388)).
24+- Add custom type errors for partially applied combinators
25+ ([#1289](https://github.com/haskell-servant/servant/pull/1289),
26+ [#1486](https://github.com/haskell-servant/servant/pull/1486)).
27+
4280.18.3
529------
630
servant-server/example/greet.hs+16−2
@@ -1,5 +1,6 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DeriveGeneric #-}
3+{-# LANGUAGE RecordWildCards #-}
34{-# LANGUAGE OverloadedStrings #-}
45{-# LANGUAGE PolyKinds #-}
56{-# LANGUAGE TypeFamilies #-}
@@ -16,6 +17,7 @@ import Network.Wai
1617import Network.Wai.Handler.Warp
1718
1819import Servant
20+import Servant.Server.Generic ()
1921
2022-- * Example
2123
@@ -38,6 +40,14 @@ type TestApi =
3840 -- DELETE /greet/:greetid
3941 :<|> "greet" :> Capture "greetid" Text :> Delete '[JSON] NoContent
4042
43+ :<|> NamedRoutes OtherRoutes
44+
45+data OtherRoutes mode = OtherRoutes
46+ { version :: mode :- Get '[JSON] Int
47+ , bye :: mode :- "bye" :> Capture "name" Text :> Get '[JSON] Text
48+ }
49+ deriving Generic
50+
4151testApi :: Proxy TestApi
4252testApi = Proxy
4353
@@ -48,9 +58,13 @@ testApi = Proxy
4858--
4959-- Each handler runs in the 'Handler' monad.
5060server :: Server TestApi
51−server = helloH :<|> postGreetH :<|> deleteGreetH
61+server = helloH :<|> postGreetH :<|> deleteGreetH :<|> otherRoutes
62+ where otherRoutes = OtherRoutes {..}
63+
64+ bye name = pure $ "Bye, " <> name <> " !"
65+ version = pure 42
5266
53− where helloH name Nothing = helloH name (Just False)
67+ helloH name Nothing = helloH name (Just False)
5468 helloH name (Just False) = return . Greet $ "Hello, " <> name
5569 helloH name (Just True) = return . Greet . toUpper $ "Hello, " <> name
5670
servant-server/servant-server.cabal+11−10
@@ -1,6 +1,6 @@
1−cabal-version: >=1.10
1+cabal-version: 2.2
22name: servant-server
3−version: 0.18.3
3+version: 0.19.1
44
55synopsis: A family of combinators for defining webservices APIs and serving them
66category: Servant, Web
@@ -60,24 +60,25 @@ library
6060 -- Bundled with GHC: Lower bound to not force re-installs
6161 -- text and mtl are bundled starting with GHC-8.4
6262 build-depends:
63− base >= 4.9 && < 4.16
63+ base >= 4.9 && < 4.17
6464 , bytestring >= 0.10.8.1 && < 0.12
65+ , constraints >= 0.2 && < 0.14
6566 , containers >= 0.5.7.1 && < 0.7
6667 , mtl >= 2.2.2 && < 2.3
67− , text >= 1.2.3.0 && < 1.3
68+ , text >= 1.2.3.0 && < 2.1
6869 , transformers >= 0.5.2.0 && < 0.6
6970 , filepath >= 1.4.1.1 && < 1.5
7071
7172 -- Servant dependencies
7273 -- strict dependency as we re-export 'servant' things.
7374 build-depends:
74− servant >= 0.18.3 && < 0.18.4
75+ servant >= 0.19 && < 0.20
7576 , http-api-data >= 0.4.1 && < 0.4.4
7677
7778 -- Other dependencies: Lower bound around what is in the latest Stackage LTS.
7879 -- Here can be exceptions if we really need features from the newer versions.
7980 build-depends:
80− base-compat >= 0.10.5 && < 0.12
81+ base-compat >= 0.10.5 && < 0.13
8182 , base64-bytestring >= 1.0.0.1 && < 1.3
8283 , exceptions >= 0.10.0 && < 0.11
8384 , http-media >= 0.7.1.3 && < 0.9
@@ -113,7 +114,7 @@ executable greet
113114 , text
114115
115116 build-depends:
116− aeson >= 1.4.1.0 && < 1.6
117+ aeson >= 1.4.1.0 && < 3
117118 , warp >= 3.2.25 && < 3.4
118119
119120test-suite spec
@@ -156,9 +157,9 @@ test-suite spec
156157
157158 -- Additional dependencies
158159 build-depends:
159− aeson >= 1.4.1.0 && < 1.6
160+ aeson >= 1.4.1.0 && < 3
160161 , directory >= 1.3.0.0 && < 1.4
161− , hspec >= 2.6.0 && < 2.9
162+ , hspec >= 2.6.0 && < 2.10
162163 , hspec-wai >= 0.10.1 && < 0.12
163164 , QuickCheck >= 2.12.6.1 && < 2.15
164165 , should-not-typecheck >= 2.1.0 && < 2.2
@@ -166,4 +167,4 @@ test-suite spec
166167 , wai-extra >= 3.0.24.3 && < 3.2
167168
168169 build-tool-depends:
169− hspec-discover:hspec-discover >= 2.6.0 && <2.9
170+ hspec-discover:hspec-discover >= 2.6.0 && <2.10
servant-server/src/Servant/Server.hs+3−2
@@ -235,7 +235,7 @@ hoistServer p = hoistServerWithContext p (Proxy :: Proxy '[])
235235-- > │ └─ e/
236236-- > │ └─•
237237-- > ├─ b/
238−-- > │ └─ <capture>/
238+-- > │ └─ <x::Int>/
239239-- > │ ├─•
240240-- > │ ┆
241241-- > │ └─•
@@ -252,7 +252,8 @@ hoistServer p = hoistServerWithContext p (Proxy :: Proxy '[])
252252--
253253-- [@─•@] Leaves reflect endpoints.
254254--
255−-- [@\<capture\>/@] This is a delayed capture of a path component.
255+-- [@\<x::Int\>/@] This is a delayed capture of a single
256+-- path component named @x@, of expected type @Int@.
256257--
257258-- [@\<raw\>@] This is a part of the API we do not know anything about.
258259--
servant-server/src/Servant/Server/Generic.hs+11−18
@@ -1,12 +1,10 @@
1−{-# LANGUAGE ConstraintKinds #-}
2−{-# LANGUAGE DataKinds #-}
3−{-# LANGUAGE FlexibleContexts #-}
4−{-# LANGUAGE KindSignatures #-}
5−{-# LANGUAGE RankNTypes #-}
6−{-# LANGUAGE ScopedTypeVariables #-}
7−{-# LANGUAGE TypeFamilies #-}
8−{-# LANGUAGE TypeOperators #-}
9−{-# LANGUAGE UndecidableInstances #-}
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE FlexibleContexts #-}
3+{-# LANGUAGE RankNTypes #-}
4+{-# LANGUAGE ScopedTypeVariables #-}
5+{-# LANGUAGE TypeFamilies #-}
6+{-# LANGUAGE TypeOperators #-}
7+
108-- | @since 0.14.1
119module Servant.Server.Generic (
1210 AsServerT,
@@ -15,21 +13,15 @@ module Servant.Server.Generic (
1513 genericServeT,
1614 genericServeTWithContext,
1715 genericServer,
18− genericServerT,
16+ genericServerT
1917 ) where
2018
2119import Data.Proxy
2220 (Proxy (..))
2321
24−import Servant.API.Generic
2522import Servant.Server
26−
27−-- | A type that specifies that an API record contains a server implementation.
28−data AsServerT (m :: * -> *)
29−instance GenericMode (AsServerT m) where
30− type AsServerT m :- api = ServerT api m
31−
32−type AsServer = AsServerT Handler
23+import Servant.API.Generic
24+import Servant.Server.Internal
3325
3426-- | Transform a record of routes into a WAI 'Application'.
3527genericServe
@@ -97,3 +89,4 @@ genericServerT
9789 => routes (AsServerT m)
9890 -> ToServant routes (AsServerT m)
9991genericServerT = toServant
92+
servant-server/src/Servant/Server/Internal.hs+107−53
@@ -1,23 +1,22 @@
1−{-# LANGUAGE CPP #-}
1+{-# LANGUAGE AllowAmbiguousTypes #-}
22{-# LANGUAGE ConstraintKinds #-}
33{-# LANGUAGE DataKinds #-}
44{-# LANGUAGE DeriveDataTypeable #-}
55{-# LANGUAGE FlexibleContexts #-}
66{-# LANGUAGE FlexibleInstances #-}
7+{-# LANGUAGE InstanceSigs #-}
78{-# LANGUAGE MultiParamTypeClasses #-}
89{-# LANGUAGE OverloadedStrings #-}
910{-# LANGUAGE PolyKinds #-}
11+{-# LANGUAGE QuantifiedConstraints #-}
1012{-# LANGUAGE RankNTypes #-}
1113{-# LANGUAGE ScopedTypeVariables #-}
1214{-# LANGUAGE TupleSections #-}
15+{-# LANGUAGE TypeApplications #-}
1316{-# LANGUAGE TypeFamilies #-}
1417{-# LANGUAGE TypeOperators #-}
1518{-# LANGUAGE UndecidableInstances #-}
1619
17−#if MIN_VERSION_base(4,9,0) && __GLASGOW_HASKELL__ >= 802
18−#define HAS_TYPE_ERROR
19−#endif
20−
2120module Servant.Server.Internal
2221 ( module Servant.Server.Internal
2322 , module Servant.Server.Internal.BasicAuth
@@ -42,6 +41,7 @@ import qualified Data.ByteString as B
4241import qualified Data.ByteString.Builder as BB
4342import qualified Data.ByteString.Char8 as BC8
4443import qualified Data.ByteString.Lazy as BL
44+import Data.Constraint (Constraint, Dict(..))
4545import Data.Either
4646 (partitionEithers)
4747import Data.Maybe
@@ -54,8 +54,9 @@ import Data.Tagged
5454 (Tagged (..), retag, untag)
5555import qualified Data.Text as T
5656import Data.Typeable
57+import GHC.Generics
5758import GHC.TypeLits
58− (KnownNat, KnownSymbol, natVal, symbolVal)
59+ (KnownNat, KnownSymbol, TypeError, symbolVal)
5960import qualified Network.HTTP.Media as NHM
6061import Network.HTTP.Types hiding
6162 (Header, ResponseHeaders)
@@ -75,7 +76,8 @@ import Servant.API
7576 QueryParam', QueryParams, Raw, ReflectMethod (reflectMethod),
7677 RemoteHost, ReqBody', SBool (..), SBoolI (..), SourceIO,
7778 Stream, StreamBody', Summary, ToSourceIO (..), Vault, Verb,
78− WithNamedContext)
79+ WithNamedContext, NamedRoutes)
80+import Servant.API.Generic (GenericMode(..), ToServant, ToServantApi, GServantProduct, toServant, fromServant)
7981import Servant.API.ContentTypes
8082 (AcceptHeader (..), AllCTRender (..), AllCTUnrender (..),
8183 AllMime, MimeRender (..), MimeUnrender (..), NoContent,
@@ -85,7 +87,10 @@ import Servant.API.Modifiers
8587 unfoldRequestArgument)
8688import Servant.API.ResponseHeaders
8789 (GetHeaders, Headers, getHeaders, getResponse)
90+import Servant.API.Status
91+ (statusFromNat)
8892import qualified Servant.Types.SourceT as S
93+import Servant.API.TypeErrors
8994import Web.HttpApiData
9095 (FromHttpApiData, parseHeader, parseQueryParam, parseUrlPiece,
9196 parseUrlPieces)
@@ -101,12 +106,10 @@ import Servant.Server.Internal.RouteResult
101106import Servant.Server.Internal.RoutingApplication
102107import Servant.Server.Internal.ServerError
103108
104−#ifdef HAS_TYPE_ERROR
105109import GHC.TypeLits
106110 (ErrorMessage (..), TypeError)
107111import Servant.API.TypeLevel
108112 (AtLeastOneFragment, FragmentUnique)
109−#endif
110113
111114class HasServer api context where
112115 type ServerT api (m :: * -> *) :: *
@@ -170,7 +173,7 @@ instance (HasServer a context, HasServer b context) => HasServer (a :<|> b) cont
170173-- > server = getBook
171174-- > where getBook :: Text -> Handler Book
172175-- > getBook isbn = ...
173−instance (KnownSymbol capture, FromHttpApiData a
176+instance (KnownSymbol capture, FromHttpApiData a, Typeable a
174177 , HasServer api context, SBoolI (FoldLenient mods)
175178 , HasContextEntry (MkContextWithErrorFormatter context) ErrorFormatters
176179 )
@@ -182,7 +185,7 @@ instance (KnownSymbol capture, FromHttpApiData a
182185 hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s
183186
184187 route Proxy context d =
185− CaptureRouter $
188+ CaptureRouter [hint] $
186189 route (Proxy :: Proxy api)
187190 context
188191 (addCapture d $ \ txt -> withRequest $ \ request ->
@@ -194,6 +197,7 @@ instance (KnownSymbol capture, FromHttpApiData a
194197 where
195198 rep = typeRep (Proxy :: Proxy Capture')
196199 formatError = urlParseErrorFormatter $ getContextEntry (mkContextWithErrorFormatter context)
200+ hint = CaptureHint (T.pack $ symbolVal $ Proxy @capture) (typeRep (Proxy :: Proxy a))
197201
198202-- | If you use 'CaptureAll' in one of the endpoints for your API,
199203-- this automatically requires your server-side handler to be a
@@ -212,7 +216,7 @@ instance (KnownSymbol capture, FromHttpApiData a
212216-- > server = getSourceFile
213217-- > where getSourceFile :: [Text] -> Handler Book
214218-- > getSourceFile pathSegments = ...
215−instance (KnownSymbol capture, FromHttpApiData a
219+instance (KnownSymbol capture, FromHttpApiData a, Typeable a
216220 , HasServer api context
217221 , HasContextEntry (MkContextWithErrorFormatter context) ErrorFormatters
218222 )
@@ -224,7 +228,7 @@ instance (KnownSymbol capture, FromHttpApiData a
224228 hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s
225229
226230 route Proxy context d =
227− CaptureAllRouter $
231+ CaptureAllRouter [hint] $
228232 route (Proxy :: Proxy api)
229233 context
230234 (addCapture d $ \ txts -> withRequest $ \ request ->
@@ -235,6 +239,7 @@ instance (KnownSymbol capture, FromHttpApiData a
235239 where
236240 rep = typeRep (Proxy :: Proxy CaptureAll)
237241 formatError = urlParseErrorFormatter $ getContextEntry (mkContextWithErrorFormatter context)
242+ hint = CaptureHint (T.pack $ symbolVal $ Proxy @capture) (typeRep (Proxy :: Proxy [a]))
238243
239244allowedMethodHead :: Method -> Request -> Bool
240245allowedMethodHead method request = method == methodGet && requestMethod request == methodHead
@@ -298,7 +303,7 @@ instance {-# OVERLAPPABLE #-}
298303
299304 route Proxy _ = methodRouter ([],) method (Proxy :: Proxy ctypes) status
300305 where method = reflectMethod (Proxy :: Proxy method)
301− status = toEnum . fromInteger $ natVal (Proxy :: Proxy status)
306+ status = statusFromNat (Proxy :: Proxy status)
302307
303308instance {-# OVERLAPPING #-}
304309 ( AllCTRender ctypes a, ReflectMethod method, KnownNat status
@@ -310,7 +315,7 @@ instance {-# OVERLAPPING #-}
310315
311316 route Proxy _ = methodRouter (\x -> (getHeaders x, getResponse x)) method (Proxy :: Proxy ctypes) status
312317 where method = reflectMethod (Proxy :: Proxy method)
313− status = toEnum . fromInteger $ natVal (Proxy :: Proxy status)
318+ status = statusFromNat (Proxy :: Proxy status)
314319
315320instance (ReflectMethod method) =>
316321 HasServer (NoContentVerb method) context where
@@ -331,7 +336,7 @@ instance {-# OVERLAPPABLE #-}
331336
332337 route Proxy _ = streamRouter ([],) method status (Proxy :: Proxy framing) (Proxy :: Proxy ctype)
333338 where method = reflectMethod (Proxy :: Proxy method)
334− status = toEnum . fromInteger $ natVal (Proxy :: Proxy status)
339+ status = statusFromNat (Proxy :: Proxy status)
335340
336341
337342instance {-# OVERLAPPING #-}
@@ -345,7 +350,7 @@ instance {-# OVERLAPPING #-}
345350
346351 route Proxy _ = streamRouter (\x -> (getHeaders x, getResponse x)) method status (Proxy :: Proxy framing) (Proxy :: Proxy ctype)
347352 where method = reflectMethod (Proxy :: Proxy method)
348− status = toEnum . fromInteger $ natVal (Proxy :: Proxy status)
353+ status = statusFromNat (Proxy :: Proxy status)
349354
350355
351356streamRouter :: forall ctype a c chunk env framing. (MimeRender ctype chunk, FramingRender framing, ToSourceIO chunk a) =>
@@ -784,7 +789,7 @@ instance ( KnownSymbol realm
784789-- * helpers
785790
786791ct_wildcard :: B.ByteString
787−ct_wildcard = "*" <> "/" <> "*" -- Because CPP
792+ct_wildcard = "*" <> "/" <> "*"
788793
789794getAcceptHeader :: Request -> AcceptHeader
790795getAcceptHeader = AcceptHeader . fromMaybe ct_wildcard . lookup hAccept . requestHeaders
@@ -812,39 +817,15 @@ instance (HasContextEntry context (NamedContext name subContext), HasServer subA
812817 hoistServerWithContext _ _ nt s = hoistServerWithContext (Proxy :: Proxy subApi) (Proxy :: Proxy subContext) nt s
813818
814819-------------------------------------------------------------------------------
815−-- TypeError helpers
820+-- Custom type errors
816821-------------------------------------------------------------------------------
817822
818−#ifdef HAS_TYPE_ERROR
819−-- | This instance catches mistakes when there are non-saturated
820−-- type applications on LHS of ':>'.
821−--
822−-- >>> serve (Proxy :: Proxy (Capture "foo" :> Get '[JSON] Int)) (error "...")
823−-- ...
824−-- ...Expected something of kind Symbol or *, got: k -> l on the LHS of ':>'.
825−-- ...Maybe you haven't applied enough arguments to
826−-- ...Capture' '[] "foo"
827−-- ...
828−--
829−-- >>> undefined :: Server (Capture "foo" :> Get '[JSON] Int)
830−-- ...
831−-- ...Expected something of kind Symbol or *, got: k -> l on the LHS of ':>'.
832−-- ...Maybe you haven't applied enough arguments to
833−-- ...Capture' '[] "foo"
834−-- ...
835−--
836−instance TypeError (HasServerArrowKindError arr) => HasServer ((arr :: k -> l) :> api) context
823+-- Erroring instance for 'HasServer' when a combinator is not fully applied
824+instance TypeError (PartialApplication HasServer arr) => HasServer ((arr :: a -> b) :> sub) context
837825 where
838− type ServerT (arr :> api) m = TypeError (HasServerArrowKindError arr)
839− -- it doesn't really matter what sub route we peak
840− route _ _ _ = error "servant-server panic: impossible happened in HasServer (arr :> api)"
841− hoistServerWithContext _ _ _ = id
842−
843−-- Cannot have TypeError here, otherwise use of this symbol will error :)
844−type HasServerArrowKindError arr =
845− 'Text "Expected something of kind Symbol or *, got: k -> l on the LHS of ':>'."
846− ':$$: 'Text "Maybe you haven't applied enough arguments to"
847− ':$$: 'ShowType arr
826+ type ServerT (arr :> sub) _ = TypeError (PartialApplication (HasServer :: * -> [*] -> Constraint) arr)
827+ route = error "unreachable"
828+ hoistServerWithContext _ _ _ _ = error "unreachable"
848829
849830-- | This instance prevents from accidentally using '->' instead of ':>'
850831--
@@ -878,7 +859,15 @@ type HasServerArrowTypeError a b =
878859 ':$$: 'ShowType a
879860 ':$$: 'Text "and"
880861 ':$$: 'ShowType b
881−#endif
862+
863+-- Erroring instances for 'HasServer' for unknown API combinators
864+
865+-- XXX: This omits the @context@ parameter, e.g.:
866+--
867+-- "There is no instance for HasServer (Bool :> …)". Do we care ?
868+instance {-# OVERLAPPABLE #-} TypeError (NoInstanceForSub HasServer ty) => HasServer (ty :> sub) context
869+
870+instance {-# OVERLAPPABLE #-} TypeError (NoInstanceFor (HasServer api context)) => HasServer api context
882871
883872-- | Ignore @'Fragment'@ in server handlers.
884873-- See <https://ietf.org/rfc/rfc2616.html#section-15.1.3> for more details.
@@ -891,11 +880,7 @@ type HasServerArrowTypeError a b =
891880-- > server = getBooks
892881-- > where getBooks :: Handler [Book]
893882-- > getBooks = ...return all books...
894−#ifdef HAS_TYPE_ERROR
895883instance (AtLeastOneFragment api, FragmentUnique (Fragment a1 :> api), HasServer api context)
896−#else
897−instance (HasServer api context)
898−#endif
899884 => HasServer (Fragment a1 :> api) context where
900885 type ServerT (Fragment a1 :> api) m = ServerT api m
901886
@@ -905,3 +890,72 @@ instance (HasServer api context)
905890
906891-- $setup
907892-- >>> import Servant
893+
894+-- | A type that specifies that an API record contains a server implementation.
895+data AsServerT (m :: * -> *)
896+instance GenericMode (AsServerT m) where
897+ type AsServerT m :- api = ServerT api m
898+
899+type AsServer = AsServerT Handler
900+
901+
902+-- | Set of constraints required to convert to / from vanilla server types.
903+type GServerConstraints api m =
904+ ( ToServant api (AsServerT m) ~ ServerT (ToServantApi api) m
905+ , GServantProduct (Rep (api (AsServerT m)))
906+ )
907+
908+-- | This class is a necessary evil: in the implementation of 'HasServer' for
909+-- @'NamedRoutes' api@, we essentially need the quantified constraint @forall
910+-- m. 'GServerConstraints' m@ to hold.
911+--
912+-- We cannot require do that directly as the definition of 'GServerConstraints'
913+-- contains type family applications ('Rep' and 'ServerT'). The trick is to hide
914+-- those type family applications behind a typeclass providing evidence for
915+-- @'GServerConstraints' api m@ in the form of a dictionary, and require that
916+-- @forall m. 'GServer' api m@ instead.
917+--
918+-- Users shouldn't have to worry about this class, as the only possible instance
919+-- is provided in this module for all record APIs.
920+
921+class GServer (api :: * -> *) (m :: * -> *) where
922+ gServerProof :: Dict (GServerConstraints api m)
923+
924+instance
925+ ( ToServant api (AsServerT m) ~ ServerT (ToServantApi api) m
926+ , GServantProduct (Rep (api (AsServerT m)))
927+ ) => GServer api m where
928+ gServerProof = Dict
929+
930+instance
931+ ( HasServer (ToServantApi api) context
932+ , forall m. Generic (api (AsServerT m))
933+ , forall m. GServer api m
934+ ) => HasServer (NamedRoutes api) context where
935+
936+ type ServerT (NamedRoutes api) m = api (AsServerT m)
937+
938+ route
939+ :: Proxy (NamedRoutes api)
940+ -> Context context
941+ -> Delayed env (api (AsServerT Handler))
942+ -> Router env
943+ route _ ctx delayed =
944+ case gServerProof @api @Handler of
945+ Dict -> route (Proxy @(ToServantApi api)) ctx (toServant <$> delayed)
946+
947+ hoistServerWithContext
948+ :: forall m n. Proxy (NamedRoutes api)
949+ -> Proxy context
950+ -> (forall x. m x -> n x)
951+ -> api (AsServerT m)
952+ -> api (AsServerT n)
953+ hoistServerWithContext _ pctx nat server =
954+ case (gServerProof @api @m, gServerProof @api @n) of
955+ (Dict, Dict) ->
956+ fromServant servantSrvN
957+ where
958+ servantSrvM :: ServerT (ToServantApi api) m =
959+ toServant server
960+ servantSrvN :: ServerT (ToServantApi api) n =
961+ hoistServerWithContext (Proxy @(ToServantApi api)) pctx nat servantSrvM
servant-server/src/Servant/Server/Internal/Handler.hs+7−2
@@ -13,17 +13,19 @@ import Control.Monad.Base
1313import Control.Monad.Catch
1414 (MonadCatch, MonadMask, MonadThrow)
1515import Control.Monad.Error.Class
16− (MonadError)
16+ (MonadError, throwError)
1717import Control.Monad.IO.Class
1818 (MonadIO)
1919import Control.Monad.Trans.Control
2020 (MonadBaseControl (..))
2121import Control.Monad.Trans.Except
2222 (ExceptT, runExceptT)
23+import Data.String
24+ (fromString)
2325import GHC.Generics
2426 (Generic)
2527import Servant.Server.Internal.ServerError
26− (ServerError)
28+ (ServerError, errBody, err500)
2729
2830newtype Handler a = Handler { runHandler' :: ExceptT ServerError IO a }
2931 deriving
@@ -32,6 +34,9 @@ newtype Handler a = Handler { runHandler' :: ExceptT ServerError IO a }
3234 , MonadThrow, MonadCatch, MonadMask
3335 )
3436
37+instance MonadFail Handler where
38+ fail str = throwError err500 { errBody = fromString str }
39+
3540instance MonadBase IO Handler where
3641 liftBase = Handler . liftBase
3742
servant-server/src/Servant/Server/Internal/Router.hs+31−14
@@ -9,12 +9,16 @@ import Prelude.Compat
99
1010import Data.Function
1111 (on)
12+import Data.List
13+ (nub)
1214import Data.Map
1315 (Map)
1416import qualified Data.Map as M
1517import Data.Text
1618 (Text)
1719import qualified Data.Text as T
20+import Data.Typeable
21+ (TypeRep)
1822import Network.Wai
1923 (Response, pathInfo)
2024import Servant.Server.Internal.ErrorFormatter
@@ -24,6 +28,18 @@ import Servant.Server.Internal.ServerError
2428
2529type Router env = Router' env RoutingApplication
2630
31+data CaptureHint = CaptureHint
32+ { captureName :: Text
33+ , captureType :: TypeRep
34+ }
35+ deriving (Show, Eq)
36+
37+toCaptureTag :: CaptureHint -> Text
38+toCaptureTag hint = captureName hint <> "::" <> (T.pack . show) (captureType hint)
39+
40+toCaptureTags :: [CaptureHint] -> Text
41+toCaptureTags hints = "<" <> T.intercalate "|" (map toCaptureTag hints) <> ">"
42+
2743-- | Internal representation of a router.
2844--
2945-- The first argument describes an environment type that is
@@ -36,10 +52,10 @@ data Router' env a =
3652 -- ^ the map contains routers for subpaths (first path component used
3753 -- for lookup and removed afterwards), the list contains handlers
3854 -- for the empty path, to be tried in order
39− | CaptureRouter (Router' (Text, env) a)
55+ | CaptureRouter [CaptureHint] (Router' (Text, env) a)
4056 -- ^ first path component is passed to the child router in its
4157 -- environment and removed afterwards
42− | CaptureAllRouter (Router' ([Text], env) a)
58+ | CaptureAllRouter [CaptureHint] (Router' ([Text], env) a)
4359 -- ^ all path components are passed to the child router in its
4460 -- environment and are removed afterwards
4561 | RawRouter (env -> a)
@@ -69,8 +85,8 @@ leafRouter l = StaticRouter M.empty [l]
6985choice :: Router' env a -> Router' env a -> Router' env a
7086choice (StaticRouter table1 ls1) (StaticRouter table2 ls2) =
7187 StaticRouter (M.unionWith choice table1 table2) (ls1 ++ ls2)
72−choice (CaptureRouter router1) (CaptureRouter router2) =
73− CaptureRouter (choice router1 router2)
88+choice (CaptureRouter hints1 router1) (CaptureRouter hints2 router2) =
89+ CaptureRouter (nub $ hints1 ++ hints2) (choice router1 router2)
7490choice router1 (Choice router2 router3) = Choice (choice router1 router2) router3
7591choice router1 router2 = Choice router1 router2
7692
@@ -84,7 +100,7 @@ choice router1 router2 = Choice router1 router2
84100--
85101data RouterStructure =
86102 StaticRouterStructure (Map Text RouterStructure) Int
87− | CaptureRouterStructure RouterStructure
103+ | CaptureRouterStructure [CaptureHint] RouterStructure
88104 | RawRouterStructure
89105 | ChoiceStructure RouterStructure RouterStructure
90106 deriving (Eq, Show)
@@ -98,11 +114,11 @@ data RouterStructure =
98114routerStructure :: Router' env a -> RouterStructure
99115routerStructure (StaticRouter m ls) =
100116 StaticRouterStructure (fmap routerStructure m) (length ls)
101−routerStructure (CaptureRouter router) =
102− CaptureRouterStructure $
117+routerStructure (CaptureRouter hints router) =
118+ CaptureRouterStructure hints $
103119 routerStructure router
104−routerStructure (CaptureAllRouter router) =
105− CaptureRouterStructure $
120+routerStructure (CaptureAllRouter hints router) =
121+ CaptureRouterStructure hints $
106122 routerStructure router
107123routerStructure (RawRouter _) =
108124 RawRouterStructure
@@ -114,8 +130,8 @@ routerStructure (Choice r1 r2) =
114130-- | Compare the structure of two routers.
115131--
116132sameStructure :: Router' env a -> Router' env b -> Bool
117−sameStructure r1 r2 =
118− routerStructure r1 == routerStructure r2
133+sameStructure router1 router2 =
134+ routerStructure router1 == routerStructure router2
119135
120136-- | Provide a textual representation of the
121137-- structure of a router.
@@ -126,7 +142,8 @@ routerLayout router =
126142 where
127143 mkRouterLayout :: Bool -> RouterStructure -> [Text]
128144 mkRouterLayout c (StaticRouterStructure m n) = mkSubTrees c (M.toList m) n
129− mkRouterLayout c (CaptureRouterStructure r) = mkSubTree c "<capture>" (mkRouterLayout False r)
145+ mkRouterLayout c (CaptureRouterStructure hints r) =
146+ mkSubTree c (toCaptureTags hints) (mkRouterLayout False r)
130147 mkRouterLayout c RawRouterStructure =
131148 if c then ["├─ <raw>"] else ["└─ <raw>"]
132149 mkRouterLayout c (ChoiceStructure r1 r2) =
@@ -169,7 +186,7 @@ runRouterEnv fmt router env request respond =
169186 -> let request' = request { pathInfo = rest }
170187 in runRouterEnv fmt router' env request' respond
171188 _ -> respond $ Fail $ fmt request
172− CaptureRouter router' ->
189+ CaptureRouter _ router' ->
173190 case pathInfo request of
174191 [] -> respond $ Fail $ fmt request
175192 -- This case is to handle trailing slashes.
@@ -177,7 +194,7 @@ runRouterEnv fmt router env request respond =
177194 first : rest
178195 -> let request' = request { pathInfo = rest }
179196 in runRouterEnv fmt router' (first, env) request' respond
180− CaptureAllRouter router' ->
197+ CaptureAllRouter _ router' ->
181198 let segments = pathInfo request
182199 request' = request { pathInfo = [] }
183200 in runRouterEnv fmt router' (segments, env) request' respond
servant-server/test/Servant/Server/RouterSpec.hs+127−6
@@ -9,7 +9,9 @@ import Control.Monad
99import Data.Proxy
1010 (Proxy (..))
1111import Data.Text
12− (unpack)
12+ (Text, unpack)
13+import Data.Typeable
14+ (typeRep)
1315import Network.HTTP.Types
1416 (Status (..))
1517import Network.Wai
@@ -27,6 +29,7 @@ spec :: Spec
2729spec = describe "Servant.Server.Internal.Router" $ do
2830 routerSpec
2931 distributivitySpec
32+ serverLayoutSpec
3033
3134routerSpec :: Spec
3235routerSpec = do
@@ -51,7 +54,7 @@ routerSpec = do
5154 toApp = toApplication . runRouter (const err404)
5255
5356 cap :: Router ()
54− cap = CaptureRouter $
57+ cap = CaptureRouter [hint] $
5558 let delayed = addCapture (emptyDelayed $ Route pure) (const $ delayedFail err400)
5659 in leafRouter
5760 $ \env req res ->
@@ -59,6 +62,9 @@ routerSpec = do
5962 . const
6063 $ Route success
6164
65+ hint :: CaptureHint
66+ hint = CaptureHint "anything" $ typeRep (Proxy :: Proxy ())
67+
6268 router :: Router ()
6369 router = leafRouter (\_ _ res -> res $ Route success)
6470 `Choice` cap
@@ -98,12 +104,30 @@ distributivitySpec =
98104 it "properly handles mixing static paths at different levels" $ do
99105 level `shouldHaveSameStructureAs` levelRef
100106
107+serverLayoutSpec :: Spec
108+serverLayoutSpec =
109+ describe "serverLayout" $ do
110+ it "correctly represents the example API" $ do
111+ exampleLayout `shouldHaveLayout` expectedExampleLayout
112+ it "aggregates capture hints when different" $ do
113+ captureDifferentTypes `shouldHaveLayout` expectedCaptureDifferentTypes
114+ it "nubs capture hints when equal" $ do
115+ captureSameType `shouldHaveLayout` expectedCaptureSameType
116+ it "properly displays CaptureAll hints" $ do
117+ captureAllLayout `shouldHaveLayout` expectedCaptureAllLayout
118+
101119shouldHaveSameStructureAs ::
102120 (HasServer api1 '[], HasServer api2 '[]) => Proxy api1 -> Proxy api2 -> Expectation
103121shouldHaveSameStructureAs p1 p2 =
104122 unless (sameStructure (makeTrivialRouter p1) (makeTrivialRouter p2)) $
105123 expectationFailure ("expected:\n" ++ unpack (layout p2) ++ "\nbut got:\n" ++ unpack (layout p1))
106124
125+shouldHaveLayout ::
126+ (HasServer api '[]) => Proxy api -> Text -> Expectation
127+shouldHaveLayout p l =
128+ unless (routerLayout (makeTrivialRouter p) == l) $
129+ expectationFailure ("expected:\n" ++ unpack l ++ "\nbut got:\n" ++ unpack (layout p))
130+
107131makeTrivialRouter :: (HasServer layout '[]) => Proxy layout -> Router ()
108132makeTrivialRouter p =
109133 route p EmptyContext (emptyDelayed (FailFatal err501))
@@ -144,12 +168,12 @@ staticRef = Proxy
144168-- structure:
145169
146170type Dynamic =
147− "a" :> Capture "foo" Int :> "b" :> End
148− :<|> "a" :> Capture "bar" Bool :> "c" :> End
149− :<|> "a" :> Capture "baz" Char :> "d" :> End
171+ "a" :> Capture "foo" Int :> "b" :> End
172+ :<|> "a" :> Capture "foo" Int :> "c" :> End
173+ :<|> "a" :> Capture "foo" Int :> "d" :> End
150174
151175type DynamicRef =
152− "a" :> Capture "anything" () :>
176+ "a" :> Capture "foo" Int :>
153177 ("b" :> End :<|> "c" :> End :<|> "d" :> End)
154178
155179dynamic :: Proxy Dynamic
@@ -339,3 +363,100 @@ level = Proxy
339363
340364levelRef :: Proxy LevelRef
341365levelRef = Proxy
366+
367+-- The example API for the 'layout' function.
368+-- Should get factorized by the 'choice' smart constructor.
369+type ExampleLayout =
370+ "a" :> "d" :> Get '[JSON] NoContent
371+ :<|> "b" :> Capture "x" Int :> Get '[JSON] Bool
372+ :<|> "c" :> Put '[JSON] Bool
373+ :<|> "a" :> "e" :> Get '[JSON] Int
374+ :<|> "b" :> Capture "x" Int :> Put '[JSON] Bool
375+ :<|> Raw
376+
377+exampleLayout :: Proxy ExampleLayout
378+exampleLayout = Proxy
379+
380+-- The expected representation of the example API layout
381+--
382+expectedExampleLayout :: Text
383+expectedExampleLayout =
384+ "/\n\
385+ \├─ a/\n\
386+ \│ ├─ d/\n\
387+ \│ │ └─•\n\
388+ \│ └─ e/\n\
389+ \│ └─•\n\
390+ \├─ b/\n\
391+ \│ └─ <x::Int>/\n\
392+ \│ ├─•\n\
393+ \│ ┆\n\
394+ \│ └─•\n\
395+ \├─ c/\n\
396+ \│ └─•\n\
397+ \┆\n\
398+ \└─ <raw>\n"
399+
400+-- A capture API with all capture types being the same
401+--
402+type CaptureSameType =
403+ "a" :> Capture "foo" Int :> "b" :> End
404+ :<|> "a" :> Capture "foo" Int :> "c" :> End
405+ :<|> "a" :> Capture "foo" Int :> "d" :> End
406+
407+captureSameType :: Proxy CaptureSameType
408+captureSameType = Proxy
409+
410+-- The expected representation of the CaptureSameType API layout.
411+--
412+expectedCaptureSameType :: Text
413+expectedCaptureSameType =
414+ "/\n\
415+ \└─ a/\n\
416+ \ └─ <foo::Int>/\n\
417+ \ ├─ b/\n\
418+ \ │ └─•\n\
419+ \ ├─ c/\n\
420+ \ │ └─•\n\
421+ \ └─ d/\n\
422+ \ └─•\n"
423+
424+-- A capture API capturing different types
425+--
426+type CaptureDifferentTypes =
427+ "a" :> Capture "foo" Int :> "b" :> End
428+ :<|> "a" :> Capture "bar" Bool :> "c" :> End
429+ :<|> "a" :> Capture "baz" Char :> "d" :> End
430+
431+captureDifferentTypes :: Proxy CaptureDifferentTypes
432+captureDifferentTypes = Proxy
433+
434+-- The expected representation of the CaptureDifferentTypes API layout.
435+--
436+expectedCaptureDifferentTypes :: Text
437+expectedCaptureDifferentTypes =
438+ "/\n\
439+ \└─ a/\n\
440+ \ └─ <foo::Int|bar::Bool|baz::Char>/\n\
441+ \ ├─ b/\n\
442+ \ │ └─•\n\
443+ \ ├─ c/\n\
444+ \ │ └─•\n\
445+ \ └─ d/\n\
446+ \ └─•\n"
447+
448+-- An API with a CaptureAll part
449+
450+type CaptureAllLayout = "a" :> CaptureAll "foos" Int :> End
451+
452+captureAllLayout :: Proxy CaptureAllLayout
453+captureAllLayout = Proxy
454+
455+-- The expected representation of the CaptureAllLayout API.
456+--
457+expectedCaptureAllLayout :: Text
458+expectedCaptureAllLayout =
459+ "/\n\
460+ \└─ a/\n\
461+ \ └─ <foos::[Int]>/\n\
462+ \ └─•\n"
servant-swagger/CHANGELOG.mdnew file+143
@@ -0,0 +1,143 @@
1+1.1.9
2+-------
3+
4+* Support `servant-0.18`
5+
6+1.1.8
7+-------
8+
9+* Support `servant-0.17`
10+
11+1.1.7.1
12+-------
13+
14+* Support `swagger2-2.4`
15+
16+1.1.7
17+-----
18+
19+* Support servant-0.15
20+ - support for 'Stream' and 'StreamBody' combinators
21+ - orphan 'ToSchema (SourceT m a)' instance
22+* Fix BodyTypes to work with generalized ReqBody'
23+ [#88](https://github.com/haskell-servant/servant-swagger/pull/88)
24+
25+1.1.6
26+-----
27+
28+* Fixes:
29+ * `validateEveryToJSON` now prints validation errors
30+
31+* Notes:
32+ * GHC-8.6 compatible release
33+
34+1.1.5
35+-----
36+
37+* Notes:
38+ * `servant-0.13` compatible release
39+ * Drops compatibility with previous `servant` versions.
40+
41+1.1.4
42+-----
43+
44+* Notes:
45+ * `servant-0.12` compatible release
46+
47+1.1.3.1
48+---
49+
50+* Notes:
51+ * GHC-8.2 compatible release
52+
53+1.1.3
54+---
55+
56+* Notes:
57+ * `servant-0.11` compatible release
58+
59+1.1.2.1
60+---
61+
62+* Notes:
63+ * `servant-0.10` compatible release
64+
65+1.1.2
66+---
67+
68+* Minor fixes:
69+ * Support for aeson-1, insert-ordered-containers-0.2
70+ * CaptureAll instance
71+
72+1.1.1
73+---
74+
75+* Minor fixes:
76+ * Fix `unused-imports` and `unused-foralls` warnings;
77+ * Fix tests to match `swagger2-2.1.1` (add `example` property for `UTCTime` schema).
78+
79+1.1
80+---
81+
82+* Breaking changes:
83+ * Requires `swagger2 >= 2.1`
84+ * Requires `servant >= 0.5`
85+
86+* Notes:
87+ * GHC-8.0 compatible release
88+
89+1.0.3
90+---
91+
92+* Fixes:
93+ * Improve compile-time performance of `BodyTypes` even further (see [18e0d95](https://github.com/haskell-servant/servant-swagger/commit/18e0d95ef6fe9076dd9621cb515d8d1a189f71d3))!
94+
95+1.0.2
96+---
97+
98+* Minor changes:
99+ * Add GHC 7.8 support (see [#26](https://github.com/haskell-servant/servant-swagger/pull/26)).
100+
101+* Fixes:
102+ * Improve compile-time performance of `BodyTypes` (see [#25](https://github.com/haskell-servant/servant-swagger/issues/25)).
103+
104+1.0.1
105+---
106+
107+* Fixes:
108+ * Stop using `Data.Swagger.Internal`;
109+ * Documentation fixes (links to examples).
110+
111+1.0
112+---
113+
114+* Major changes (see [#24](https://github.com/haskell-servant/servant-swagger/pull/24)):
115+ * Switch to `swagger2-2.*`;
116+ * Add automatic `ToJSON`/`ToSchema` validation tests;
117+ * Add great documentation;
118+ * Export some type-level functions for servant API.
119+
120+* Minor changes:
121+ * Rework Todo API example;
122+ * Stop exporting `ToResponseHeader`, `AllAccept` and `AllToResponseHeader` (see [bd50db4](https://github.com/haskell-servant/servant-swagger/commit/bd50db48ca6a106e4366560ded70932d409de1e2));
123+ * Change maintainer, update authors/copyrights (see [1a62681](https://github.com/haskell-servant/servant-swagger/commit/1a6268101dc826a92c42e832e402e251c0d32147));
124+ * Include changelog and example files into `extra-source-files`.
125+
126+0.1.2
127+---
128+
129+* Fixes:
130+ * Fix default spec for `ReqBody` param to be required (see [#22](https://github.com/haskell-servant/servant-swagger/issues/22));
131+ * Set version bounds for `swagger2`.
132+
133+0.1.1
134+---
135+
136+* Fixes:
137+ * Fix `subOperations` to filter endpoints also by method (see [#18](https://github.com/haskell-servant/servant-swagger/issues/18));
138+ * Fix response schema in `ToSwagger` instance for `Header` (see [b59e557](https://github.com/haskell-servant/servant-swagger/commit/b59e557a05bc2669332c52b397879e7598747b82)).
139+
140+0.1
141+---
142+* Major changes
143+ * Use `swagger2` for data model (see [#9](https://github.com/dmjio/servant-swagger/pull/9)); this changes almost everything.
servant-swagger/LICENSEnew file+28
@@ -0,0 +1,28 @@
1+Copyright (c) 2015-2016, Servant contributors
2+All rights reserved.
3+
4+Redistribution and use in source and binary forms, with or without
5+modification, are permitted provided that the following conditions are met:
6+
7+* Redistributions of source code must retain the above copyright notice, this
8+ list of conditions and the following disclaimer.
9+
10+* Redistributions in binary form must reproduce the above copyright notice,
11+ this list of conditions and the following disclaimer in the documentation
12+ and/or other materials provided with the distribution.
13+
14+* Neither the name of servant-swagger nor the names of its
15+ contributors may be used to endorse or promote products derived from
16+ this software without specific prior written permission.
17+
18+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28+
servant-swagger/README.mdnew file+46
@@ -0,0 +1,46 @@
1+# servant-swagger
2+
3+[![Hackage](https://img.shields.io/hackage/v/servant-swagger.svg)](http://hackage.haskell.org/package/servant-swagger)
4+[![Stackage LTS](http://stackage.org/package/servant-swagger/badge/lts)](http://stackage.org/lts/package/servant-swagger)
5+[![Stackage Nightly](http://stackage.org/package/servant-swagger/badge/nightly)](http://stackage.org/nightly/package/servant-swagger)
6+
7+Swagger 2.0 conforming json for [servant](https://github.com/haskell-servant/servant) APIs.
8+
9+![servant-swagger robot](http://s16.postimg.org/rndz1wbyt/servant.png)
10+
11+### Motivation
12+
13+Swagger is a project used to describe and document RESTful APIs.
14+Unlike Servant it is language-agnostic and thus is quite popular among developers
15+in different languages. It also exists for a longer time and has more tools to work with.
16+
17+This package provides means to generate Swagger specification for a Servant API
18+and also to partially test whether API conforms with its specification.
19+
20+Generated Swagger specification then can be used for many things such as
21+- displaying interactive documentation using [Swagger UI](http://swagger.io/swagger-ui/);
22+- generating clients and servers in many languages using [Swagger Codegen](http://swagger.io/swagger-codegen/);
23+- and [many others](http://swagger.io/open-source-integrations/).
24+
25+### Usage
26+
27+Please refer to [haddock documentation](http://hackage.haskell.org/package/servant/servant-swagger).
28+
29+Some examples can be found in [`example/` directory](/example).
30+
31+### Try it out
32+
33+All generated swagger specifications can be interactively viewed on [Swagger Editor](http://editor.swagger.io/).
34+
35+Ready-to-use specification can be served as JSON and interactive API documentation
36+can be displayed using [Swagger UI](https://github.com/swagger-api/swagger-ui).
37+
38+Many Swagger tools, including server and client code generation for many languages, can be found on
39+[Swagger's Tools and Integrations page](http://swagger.io/open-source-integrations/).
40+
41+### Contributing
42+
43+We are happy to receive bug reports, fixes, documentation enhancements, and other improvements.
44+
45+Please report bugs via the [github issue tracker](https://github.com/haskell-servant/servant/issues).
46+
servant-swagger/Setup.hsnew file+33
@@ -0,0 +1,33 @@
1+{-# LANGUAGE CPP #-}
2+{-# OPTIONS_GHC -Wall #-}
3+module Main (main) where
4+
5+#ifndef MIN_VERSION_cabal_doctest
6+#define MIN_VERSION_cabal_doctest(x,y,z) 0
7+#endif
8+
9+#if MIN_VERSION_cabal_doctest(1,0,0)
10+
11+import Distribution.Extra.Doctest ( defaultMainWithDoctests )
12+main :: IO ()
13+main = defaultMainWithDoctests "doctests"
14+
15+#else
16+
17+#ifdef MIN_VERSION_Cabal
18+-- If the macro is defined, we have new cabal-install,
19+-- but for some reason we don't have cabal-doctest in package-db
20+--
21+-- Probably we are running cabal sdist, when otherwise using new-build
22+-- workflow
23+#warning You are configuring this package without cabal-doctest installed. \
24+ The doctests test-suite will not work as a result. \
25+ To fix this, install cabal-doctest before configuring.
26+#endif
27+
28+import Distribution.Simple
29+
30+main :: IO ()
31+main = defaultMain
32+
33+#endif
servant-swagger/example/LICENSEnew file+28
@@ -0,0 +1,28 @@
1+Copyright (c) 2015-2016, Servant contributors
2+All rights reserved.
3+
4+Redistribution and use in source and binary forms, with or without
5+modification, are permitted provided that the following conditions are met:
6+
7+* Redistributions of source code must retain the above copyright notice, this
8+ list of conditions and the following disclaimer.
9+
10+* Redistributions in binary form must reproduce the above copyright notice,
11+ this list of conditions and the following disclaimer in the documentation
12+ and/or other materials provided with the distribution.
13+
14+* Neither the name of servant-swagger nor the names of its
15+ contributors may be used to endorse or promote products derived from
16+ this software without specific prior written permission.
17+
18+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28+
servant-swagger/example/example.cabalnew file+62
@@ -0,0 +1,62 @@
1+cabal-version: 2.2
2+name: example
3+version: 1.0
4+synopsis: servant-swagger demonstration
5+description: servant-swagger demonstration
6+license: BSD-3-Clause
7+license-file: LICENSE
8+author: David Johnson, Nickolay Kudasov
9+maintainer: nickolay.kudasov@gmail.com
10+copyright: (c) 2015-2016, Servant contributors
11+category: Web
12+build-type: Simple
13+data-files:
14+ swagger.json
15+
16+library
17+ ghc-options: -Wall
18+ hs-source-dirs: src/
19+ exposed-modules:
20+ Todo
21+ build-depends: base
22+ , aeson
23+ , aeson-pretty
24+ , bytestring
25+ , lens
26+ , servant
27+ , servant-server
28+ , servant-swagger
29+ , swagger2
30+ , text
31+ , time
32+ default-language: Haskell2010
33+
34+executable swagger-server
35+ ghc-options: -Wall
36+ hs-source-dirs: server/
37+ main-is: Main.hs
38+ build-depends: base
39+ , example
40+ , servant-server
41+ , warp
42+ default-language: Haskell2010
43+
44+test-suite swagger-server-spec
45+ ghc-options: -Wall
46+ type: exitcode-stdio-1.0
47+ hs-source-dirs: test
48+ main-is: Spec.hs
49+ other-modules:
50+ TodoSpec
51+ Paths_example
52+ build-depends: base == 4.*
53+ , base-compat >= 0.6.0
54+ , aeson >=0.11.2.0
55+ , bytestring
56+ , example
57+ , hspec
58+ , servant-swagger
59+ , QuickCheck
60+ , quickcheck-instances
61+ default-language: Haskell2010
62+
servant-swagger/example/server/Main.hsnew file+11
@@ -0,0 +1,11 @@
1+module Main where
2+
3+import Network.Wai.Handler.Warp
4+import Servant
5+import Todo
6+
7+main :: IO ()
8+main = do
9+ putStrLn "Running on port 8000"
10+ run 8000 $ serve (Proxy :: Proxy API) server
11+
servant-swagger/example/src/Todo.hsnew file+73
@@ -0,0 +1,73 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE DeriveDataTypeable #-}
3+{-# LANGUAGE DeriveGeneric #-}
4+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
5+{-# LANGUAGE OverloadedStrings #-}
6+{-# LANGUAGE TypeOperators #-}
7+module Todo where
8+
9+import Control.Lens
10+import Data.Aeson
11+import Data.Aeson.Encode.Pretty (encodePretty)
12+import qualified Data.ByteString.Lazy.Char8 as BL8
13+import Data.Proxy
14+import Data.Swagger
15+import Data.Text (Text)
16+import Data.Time (UTCTime (..), fromGregorian)
17+import Data.Typeable (Typeable)
18+import GHC.Generics
19+import Servant
20+import Servant.Swagger
21+
22+todoAPI :: Proxy TodoAPI
23+todoAPI = Proxy
24+
25+-- | The API of a Todo service.
26+type TodoAPI
27+ = "todo" :> Get '[JSON] [Todo]
28+ :<|> "todo" :> ReqBody '[JSON] Todo :> Post '[JSON] TodoId
29+ :<|> "todo" :> Capture "id" TodoId :> Get '[JSON] Todo
30+ :<|> "todo" :> Capture "id" TodoId :> ReqBody '[JSON] Todo :> Put '[JSON] TodoId
31+
32+-- | API for serving @swagger.json@.
33+type SwaggerAPI = "swagger.json" :> Get '[JSON] Swagger
34+
35+-- | Combined API of a Todo service with Swagger documentation.
36+type API = SwaggerAPI :<|> TodoAPI
37+
38+-- | A single Todo entry.
39+data Todo = Todo
40+ { created :: UTCTime -- ^ Creation datetime.
41+ , summary :: Text -- ^ Task summary.
42+ } deriving (Show, Generic, Typeable)
43+
44+-- | A unique Todo entry ID.
45+newtype TodoId = TodoId Int
46+ deriving (Show, Generic, Typeable, ToJSON, FromHttpApiData)
47+
48+instance ToJSON Todo
49+instance FromJSON Todo
50+
51+instance ToSchema Todo where
52+ declareNamedSchema proxy = genericDeclareNamedSchema defaultSchemaOptions proxy
53+ & mapped.schema.description ?~ "This is some real Todo right here"
54+ & mapped.schema.example ?~ toJSON (Todo (UTCTime (fromGregorian 2015 12 31) 0) "get milk")
55+
56+instance ToParamSchema TodoId
57+instance ToSchema TodoId
58+
59+-- | Swagger spec for Todo API.
60+todoSwagger :: Swagger
61+todoSwagger = toSwagger todoAPI
62+ & info.title .~ "Todo API"
63+ & info.version .~ "1.0"
64+ & info.description ?~ "This is an API that tests swagger integration"
65+ & info.license ?~ ("MIT" & url ?~ URL "http://mit.com")
66+
67+-- | Combined server of a Todo service with Swagger documentation.
68+server :: Server API
69+server = return todoSwagger :<|> error "not implemented"
70+
71+-- | Output generated @swagger.json@ file for the @'TodoAPI'@.
72+writeSwaggerJSON :: IO ()
73+writeSwaggerJSON = BL8.writeFile "example/swagger.json" (encodePretty todoSwagger)
servant-swagger/example/swagger.jsonnew file+158
@@ -0,0 +1,158 @@
1+{
2+ "swagger": "2.0",
3+ "info": {
4+ "version": "1.0",
5+ "title": "Todo API",
6+ "license": {
7+ "url": "http://mit.com",
8+ "name": "MIT"
9+ },
10+ "description": "This is an API that tests swagger integration"
11+ },
12+ "definitions": {
13+ "Todo": {
14+ "example": {
15+ "summary": "get milk",
16+ "created": "2015-12-31T00:00:00Z"
17+ },
18+ "required": [
19+ "created",
20+ "summary"
21+ ],
22+ "type": "object",
23+ "description": "This is some real Todo right here",
24+ "properties": {
25+ "summary": {
26+ "type": "string"
27+ },
28+ "created": {
29+ "$ref": "#/definitions/UTCTime"
30+ }
31+ }
32+ },
33+ "UTCTime": {
34+ "example": "2016-07-22T00:00:00Z",
35+ "format": "yyyy-mm-ddThh:MM:ssZ",
36+ "type": "string"
37+ },
38+ "TodoId": {
39+ "maximum": 9223372036854775807,
40+ "minimum": -9223372036854775808,
41+ "type": "integer"
42+ }
43+ },
44+ "paths": {
45+ "/todo/{id}": {
46+ "get": {
47+ "responses": {
48+ "400": {
49+ "description": "Invalid `id`"
50+ },
51+ "200": {
52+ "schema": {
53+ "$ref": "#/definitions/Todo"
54+ },
55+ "description": ""
56+ }
57+ },
58+ "produces": [
59+ "application/json;charset=utf-8"
60+ ],
61+ "parameters": [
62+ {
63+ "maximum": 9223372036854775807,
64+ "minimum": -9223372036854775808,
65+ "required": true,
66+ "in": "path",
67+ "name": "id",
68+ "type": "integer"
69+ }
70+ ]
71+ },
72+ "put": {
73+ "consumes": [
74+ "application/json;charset=utf-8"
75+ ],
76+ "responses": {
77+ "400": {
78+ "description": "Invalid `body` or `id`"
79+ },
80+ "200": {
81+ "schema": {
82+ "$ref": "#/definitions/TodoId"
83+ },
84+ "description": ""
85+ }
86+ },
87+ "produces": [
88+ "application/json;charset=utf-8"
89+ ],
90+ "parameters": [
91+ {
92+ "maximum": 9223372036854775807,
93+ "minimum": -9223372036854775808,
94+ "required": true,
95+ "in": "path",
96+ "name": "id",
97+ "type": "integer"
98+ },
99+ {
100+ "required": true,
101+ "schema": {
102+ "$ref": "#/definitions/Todo"
103+ },
104+ "in": "body",
105+ "name": "body"
106+ }
107+ ]
108+ }
109+ },
110+ "/todo": {
111+ "post": {
112+ "consumes": [
113+ "application/json;charset=utf-8"
114+ ],
115+ "responses": {
116+ "400": {
117+ "description": "Invalid `body`"
118+ },
119+ "200": {
120+ "schema": {
121+ "$ref": "#/definitions/TodoId"
122+ },
123+ "description": ""
124+ }
125+ },
126+ "produces": [
127+ "application/json;charset=utf-8"
128+ ],
129+ "parameters": [
130+ {
131+ "required": true,
132+ "schema": {
133+ "$ref": "#/definitions/Todo"
134+ },
135+ "in": "body",
136+ "name": "body"
137+ }
138+ ]
139+ },
140+ "get": {
141+ "responses": {
142+ "200": {
143+ "schema": {
144+ "items": {
145+ "$ref": "#/definitions/Todo"
146+ },
147+ "type": "array"
148+ },
149+ "description": ""
150+ }
151+ },
152+ "produces": [
153+ "application/json;charset=utf-8"
154+ ]
155+ }
156+ }
157+ }
158+}
servant-swagger/example/test/Spec.hsnew file+1
@@ -0,0 +1 @@
1+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
servant-swagger/example/test/TodoSpec.hsnew file+28
@@ -0,0 +1,28 @@
1+{-# OPTIONS_GHC -fno-warn-orphans #-}
2+module TodoSpec where
3+
4+import Prelude ()
5+import Prelude.Compat
6+
7+import Data.Aeson
8+import qualified Data.ByteString.Lazy.Char8 as BL8
9+import Paths_example
10+import Servant.Swagger.Test
11+import Test.Hspec
12+import Test.QuickCheck
13+import Test.QuickCheck.Instances ()
14+import Todo
15+
16+spec :: Spec
17+spec = describe "Swagger" $ do
18+ context "ToJSON matches ToSchema" $ validateEveryToJSON todoAPI
19+ it "swagger.json is up-to-date" $ do
20+ path <- getDataFileName "swagger.json"
21+ swag <- eitherDecode <$> BL8.readFile path
22+ swag `shouldBe` Right todoSwagger
23+
24+instance Arbitrary Todo where
25+ arbitrary = Todo <$> arbitrary <*> arbitrary
26+
27+instance Arbitrary TodoId where
28+ arbitrary = TodoId <$> arbitrary
servant-swagger/servant-swagger.cabalnew file+127
@@ -0,0 +1,127 @@
1+cabal-version: 2.2
2+name: servant-swagger
3+version: 1.1.11
4+synopsis: Generate a Swagger/OpenAPI/OAS 2.0 specification for your servant API.
5+description:
6+ Swagger is a project used to describe and document RESTful APIs. The core of the
7+ project is the [OpenAPI Specification (OAS)](https://swagger.io/docs/specification/about/).
8+ This library implements v2.0 of the spec. Unlike Servant it is language-agnostic and thus is
9+ quite popular among developers in different languages. It has also existed for a longer time
10+ and has more helpful tooling.
11+ .
12+ This package provides means to generate a Swagger/OAS specification for a Servant API
13+ and also to partially test whether an API conforms with its specification.
14+ .
15+ Generated Swagger specification then can be used for many things such as
16+ .
17+ * displaying interactive documentation using [Swagger UI](http://swagger.io/swagger-ui/);
18+ .
19+ * generating clients and servers in many languages using [Swagger Codegen](http://swagger.io/swagger-codegen/);
20+ .
21+ * and [many others](http://swagger.io/open-source-integrations/).
22+homepage: https://github.com/haskell-servant/servant/tree/master/servant-swagger#readme
23+bug-reports: https://github.com/haskell-servant/servant/issues
24+license: BSD-3-Clause
25+license-file: LICENSE
26+author: Servant Contributors
27+maintainer: haskell-servant-maintainers@googlegroups.com
28+copyright: (c) 2015-2018, Servant contributors
29+category: Web, Servant, Swagger
30+build-type: Custom
31+tested-with: GHC ==8.6.5 || ==8.8.4 || ==8.10.2
32+
33+extra-source-files:
34+ README.md
35+ , CHANGELOG.md
36+ , example/server/*.hs
37+ , example/src/*.hs
38+ , example/test/*.hs
39+ , example/*.cabal
40+ , example/swagger.json
41+ , example/LICENSE
42+extra-doc-files:
43+ example/src/*.hs
44+ , example/test/*.hs
45+
46+source-repository head
47+ type: git
48+ location: https://github.com/haskell-servant/servant.git
49+
50+custom-setup
51+ setup-depends:
52+ base >=4.9 && <5,
53+ Cabal >= 1.24,
54+ cabal-doctest >=1.0.6 && <1.1
55+
56+library
57+ ghc-options: -Wall
58+ exposed-modules:
59+ Servant.Swagger
60+ Servant.Swagger.Test
61+ Servant.Swagger.TypeLevel
62+
63+ -- Internal modules
64+ Servant.Swagger.Internal
65+ Servant.Swagger.Internal.Orphans
66+ Servant.Swagger.Internal.Test
67+ Servant.Swagger.Internal.TypeLevel
68+ Servant.Swagger.Internal.TypeLevel.API
69+ Servant.Swagger.Internal.TypeLevel.Every
70+ Servant.Swagger.Internal.TypeLevel.TMap
71+ hs-source-dirs: src
72+ build-depends: aeson >=1.4.2.0 && <3
73+ , aeson-pretty >=0.8.7 && <0.9
74+ , base >=4.9.1.0 && <5
75+ , base-compat >=0.10.5 && <0.13
76+ , bytestring >=0.10.8.1 && <0.12
77+ , http-media >=0.7.1.3 && <0.9
78+ , insert-ordered-containers >=0.2.1.0 && <0.3
79+ , lens >=4.17 && <6
80+ , servant >=0.18.2 && <0.20
81+ , singleton-bool >=0.1.4 && <0.2
82+ , swagger2 >=2.3.0.1 && <3
83+ , text >=1.2.3.0 && <2.1
84+ , unordered-containers >=0.2.9.0 && <0.3
85+
86+ , hspec
87+ , QuickCheck
88+ default-language: Haskell2010
89+
90+test-suite doctests
91+ ghc-options: -Wall
92+ build-depends:
93+ base,
94+ directory >= 1.0,
95+ doctest >= 0.17 && <0.21,
96+ servant,
97+ QuickCheck,
98+ filepath
99+ default-language: Haskell2010
100+ hs-source-dirs: test
101+ main-is: doctests.hs
102+ type: exitcode-stdio-1.0
103+
104+test-suite spec
105+ ghc-options: -Wall
106+ type: exitcode-stdio-1.0
107+ hs-source-dirs: test
108+ main-is: Spec.hs
109+ build-tool-depends: hspec-discover:hspec-discover >=2.6.0 && <2.10
110+ build-depends: base
111+ , base-compat
112+ , aeson >=1.4.2.0 && <3
113+ , hspec >=2.6.0 && <2.10
114+ , QuickCheck
115+ , lens
116+ , lens-aeson >=1.0.2 && <1.3
117+ , servant
118+ , servant-swagger
119+ , swagger2
120+ , text
121+ , template-haskell
122+ , utf8-string >=1.0.1.1 && <1.1
123+ , time
124+ , vector
125+ other-modules:
126+ Servant.SwaggerSpec
127+ default-language: Haskell2010
servant-swagger/src/Servant/Swagger.hsnew file+188
@@ -0,0 +1,188 @@
1+-- |
2+-- Module: Servant.Swagger
3+-- License: BSD3
4+-- Maintainer: Nickolay Kudasov <nickolay@getshoptv.com>
5+-- Stability: experimental
6+--
7+-- This module provides means to generate and manipulate
8+-- Swagger specification for servant APIs.
9+--
10+-- Swagger is a project used to describe and document RESTful APIs.
11+--
12+-- The Swagger specification defines a set of files required to describe such an API.
13+-- These files can then be used by the Swagger-UI project to display the API
14+-- and Swagger-Codegen to generate clients in various languages.
15+-- Additional utilities can also take advantage of the resulting files, such as testing tools.
16+--
17+-- For more information see <http://swagger.io/ Swagger documentation>.
18+module Servant.Swagger (
19+ -- * How to use this library
20+ -- $howto
21+
22+ -- ** Generate @'Swagger'@
23+ -- $generate
24+
25+ -- ** Annotate
26+ -- $annotate
27+
28+ -- ** Test
29+ -- $test
30+
31+ -- ** Serve
32+ -- $serve
33+
34+ -- * @'HasSwagger'@ class
35+ HasSwagger(..),
36+
37+ -- * Manipulation
38+ subOperations,
39+
40+ -- * Testing
41+ validateEveryToJSON,
42+ validateEveryToJSONWithPatternChecker,
43+) where
44+
45+import Servant.Swagger.Internal
46+import Servant.Swagger.Test
47+import Servant.Swagger.Internal.Orphans ()
48+
49+-- $setup
50+-- >>> import Control.Applicative
51+-- >>> import Control.Lens
52+-- >>> import Data.Aeson
53+-- >>> import Data.Aeson.Encode.Pretty
54+-- >>> import Data.Swagger
55+-- >>> import Data.Typeable
56+-- >>> import GHC.Generics
57+-- >>> import Servant.API
58+-- >>> import System.Environment
59+-- >>> import Test.Hspec
60+-- >>> import Test.QuickCheck
61+-- >>> import qualified Data.ByteString.Lazy.Char8 as BSL8
62+-- >>> :set -XDataKinds
63+-- >>> :set -XDeriveDataTypeable
64+-- >>> :set -XDeriveGeneric
65+-- >>> :set -XGeneralizedNewtypeDeriving
66+-- >>> :set -XOverloadedStrings
67+-- >>> :set -XTypeOperators
68+-- >>> setEnv "HSPEC_COLOR" "no"
69+-- >>> data User = User { name :: String, age :: Int } deriving (Show, Generic, Typeable)
70+-- >>> newtype UserId = UserId Integer deriving (Show, Generic, Typeable, ToJSON)
71+-- >>> instance ToJSON User
72+-- >>> instance ToSchema User
73+-- >>> instance ToSchema UserId
74+-- >>> instance ToParamSchema UserId
75+-- >>> type GetUsers = Get '[JSON] [User]
76+-- >>> type GetUser = Capture "user_id" UserId :> Get '[JSON] User
77+-- >>> type PostUser = ReqBody '[JSON] User :> Post '[JSON] UserId
78+-- >>> type UserAPI = GetUsers :<|> GetUser :<|> PostUser
79+-- >>> orderedKeys = encodePretty' (defConfig { confCompare = compare, confIndent = Spaces 0 })
80+
81+-- $howto
82+--
83+-- This section explains how to use this library to generate Swagger specification,
84+-- modify it and run automatic tests for a servant API.
85+--
86+-- For the purposes of this section we will use this servant API:
87+--
88+-- >>> data User = User { name :: String, age :: Int } deriving (Show, Generic, Typeable)
89+-- >>> newtype UserId = UserId Integer deriving (Show, Generic, Typeable, ToJSON)
90+-- >>> instance ToJSON User
91+-- >>> instance ToSchema User
92+-- >>> instance ToSchema UserId
93+-- >>> instance ToParamSchema UserId
94+-- >>> type GetUsers = Get '[JSON] [User]
95+-- >>> type GetUser = Capture "user_id" UserId :> Get '[JSON] User
96+-- >>> type PostUser = ReqBody '[JSON] User :> Post '[JSON] UserId
97+-- >>> type UserAPI = GetUsers :<|> GetUser :<|> PostUser
98+--
99+-- Here we define a user API with three endpoints. @GetUsers@ endpoint returns a list of all users.
100+-- @GetUser@ returns a user given his\/her ID. @PostUser@ creates a new user and returns his\/her ID.
101+
102+-- $generate
103+-- In order to generate @'Swagger'@ specification for a servant API, just use @'toSwagger'@:
104+--
105+-- >>> BSL8.putStrLn . orderedKeys $ toSwagger (Proxy :: Proxy UserAPI)
106+-- {"definitions":{"User":{"properties":{"age":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"},"name":{"type":"string"}},"required":["name","age"],"type":"object"},"UserId":{"type":"integer"}},"info":{"title":"","version":""},"paths":{"/":{"get":{"produces":["application/json;charset=utf-8"],"responses":{"200":{"description":"","schema":{"items":{"$ref":"#/definitions/User"},"type":"array"}}}},"post":{"consumes":["application/json;charset=utf-8"],"parameters":[{"in":"body","name":"body","required":true,"schema":{"$ref":"#/definitions/User"}}],"produces":["application/json;charset=utf-8"],"responses":{"200":{"description":"","schema":{"$ref":"#/definitions/UserId"}},"400":{"description":"Invalid `body`"}}}},"/{user_id}":{"get":{"parameters":[{"in":"path","name":"user_id","required":true,"type":"integer"}],"produces":["application/json;charset=utf-8"],"responses":{"200":{"description":"","schema":{"$ref":"#/definitions/User"}},"400":{"description":"Invalid `user_id`"}}}}},"swagger":"2.0"}
107+--
108+-- By default @'toSwagger'@ will generate specification for all API routes, parameters, headers, responses and data schemas.
109+--
110+-- For some parameters it will also add 400 responses with a description mentioning parameter name.
111+--
112+-- Data schemas come from @'ToParamSchema'@ and @'ToSchema'@ classes.
113+
114+-- $annotate
115+-- While initially generated @'Swagger'@ looks good, it lacks some information it can't get from a servant API.
116+--
117+-- We can add this information using field lenses from @"Data.Swagger"@:
118+--
119+-- >>> :{
120+-- BSL8.putStrLn $ orderedKeys $ toSwagger (Proxy :: Proxy UserAPI)
121+-- & info.title .~ "User API"
122+-- & info.version .~ "1.0"
123+-- & info.description ?~ "This is an API for the Users service"
124+-- & info.license ?~ "MIT"
125+-- & host ?~ "example.com"
126+-- :}
127+-- {"definitions":{"User":{"properties":{"age":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"},"name":{"type":"string"}},"required":["name","age"],"type":"object"},"UserId":{"type":"integer"}},"host":"example.com","info":{"description":"This is an API for the Users service","license":{"name":"MIT"},"title":"User API","version":"1.0"},"paths":{"/":{"get":{"produces":["application/json;charset=utf-8"],"responses":{"200":{"description":"","schema":{"items":{"$ref":"#/definitions/User"},"type":"array"}}}},"post":{"consumes":["application/json;charset=utf-8"],"parameters":[{"in":"body","name":"body","required":true,"schema":{"$ref":"#/definitions/User"}}],"produces":["application/json;charset=utf-8"],"responses":{"200":{"description":"","schema":{"$ref":"#/definitions/UserId"}},"400":{"description":"Invalid `body`"}}}},"/{user_id}":{"get":{"parameters":[{"in":"path","name":"user_id","required":true,"type":"integer"}],"produces":["application/json;charset=utf-8"],"responses":{"200":{"description":"","schema":{"$ref":"#/definitions/User"}},"400":{"description":"Invalid `user_id`"}}}}},"swagger":"2.0"}
128+--
129+-- It is also useful to annotate or modify certain endpoints.
130+-- @'subOperations'@ provides a convenient way to zoom into a part of an API.
131+--
132+-- @'subOperations' sub api@ traverses all operations of the @api@ which are also present in @sub@.
133+-- Furthermore, @sub@ is required to be an exact sub API of @api. Otherwise it will not typecheck.
134+--
135+-- @"Data.Swagger.Operation"@ provides some useful helpers that can be used with @'subOperations'@.
136+-- One example is applying tags to certain endpoints:
137+--
138+-- >>> let getOps = subOperations (Proxy :: Proxy (GetUsers :<|> GetUser)) (Proxy :: Proxy UserAPI)
139+-- >>> let postOps = subOperations (Proxy :: Proxy PostUser) (Proxy :: Proxy UserAPI)
140+-- >>> :{
141+-- BSL8.putStrLn $ orderedKeys $ toSwagger (Proxy :: Proxy UserAPI)
142+-- & applyTagsFor getOps ["get" & description ?~ "GET operations"]
143+-- & applyTagsFor postOps ["post" & description ?~ "POST operations"]
144+-- :}
145+-- {"definitions":{"User":{"properties":{"age":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"},"name":{"type":"string"}},"required":["name","age"],"type":"object"},"UserId":{"type":"integer"}},"info":{"title":"","version":""},"paths":{"/":{"get":{"produces":["application/json;charset=utf-8"],"responses":{"200":{"description":"","schema":{"items":{"$ref":"#/definitions/User"},"type":"array"}}},"tags":["get"]},"post":{"consumes":["application/json;charset=utf-8"],"parameters":[{"in":"body","name":"body","required":true,"schema":{"$ref":"#/definitions/User"}}],"produces":["application/json;charset=utf-8"],"responses":{"200":{"description":"","schema":{"$ref":"#/definitions/UserId"}},"400":{"description":"Invalid `body`"}},"tags":["post"]}},"/{user_id}":{"get":{"parameters":[{"in":"path","name":"user_id","required":true,"type":"integer"}],"produces":["application/json;charset=utf-8"],"responses":{"200":{"description":"","schema":{"$ref":"#/definitions/User"}},"400":{"description":"Invalid `user_id`"}},"tags":["get"]}}},"swagger":"2.0","tags":[{"description":"GET operations","name":"get"},{"description":"POST operations","name":"post"}]}
146+--
147+-- This applies @\"get\"@ tag to the @GET@ endpoints and @\"post\"@ tag to the @POST@ endpoint of the User API.
148+
149+-- $test
150+-- Automatic generation of data schemas uses @'ToSchema'@ instances for the types
151+-- used in a servant API. But to encode/decode actual data servant uses different classes.
152+-- For instance in @UserAPI@ @User@ is always encoded/decoded using @'ToJSON'@ and @'FromJSON'@ instances.
153+--
154+-- To be sure your Haskell server/client handles data properly you need to check
155+-- that @'ToJSON'@ instance always generates values that satisfy schema produced
156+-- by @'ToSchema'@ instance.
157+--
158+-- With @'validateEveryToJSON'@ it is possible to test all those instances automatically,
159+-- without having to write down every type:
160+--
161+-- >>> instance Arbitrary User where arbitrary = User <$> arbitrary <*> arbitrary
162+-- >>> instance Arbitrary UserId where arbitrary = UserId <$> arbitrary
163+-- >>> hspec $ validateEveryToJSON (Proxy :: Proxy UserAPI)
164+-- <BLANKLINE>
165+-- [User]...
166+-- ...
167+-- User...
168+-- ...
169+-- UserId...
170+-- ...
171+-- Finished in ... seconds
172+-- 3 examples, 0 failures
173+--
174+-- Although servant is great, chances are that your API clients don't use Haskell.
175+-- In many cases @swagger.json@ serves as a specification, not a Haskell type.
176+--
177+-- In this cases it is a good idea to store generated and annotated @'Swagger'@ in a @swagger.json@ file
178+-- under a version control system (such as Git, Subversion, Mercurial, etc.).
179+--
180+-- It is also recommended to version API based on changes to the @swagger.json@ rather than changes
181+-- to the Haskell API.
182+--
183+-- See <example/test/TodoSpec.hs TodoSpec.hs> for an example of a complete test suite for a swagger specification.
184+
185+-- $serve
186+-- If you're implementing a server for an API, you might also want to serve its @'Swagger'@ specification.
187+--
188+-- See <example/src/Todo.hs Todo.hs> for an example of a server.
servant-swagger/src/Servant/Swagger/Internal.hsnew file+488
@@ -0,0 +1,488 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE ConstraintKinds #-}
3+{-# LANGUAGE DataKinds #-}
4+{-# LANGUAGE FlexibleContexts #-}
5+{-# LANGUAGE FlexibleInstances #-}
6+{-# LANGUAGE OverloadedLists #-}
7+{-# LANGUAGE OverloadedStrings #-}
8+{-# LANGUAGE PolyKinds #-}
9+{-# LANGUAGE RankNTypes #-}
10+{-# LANGUAGE ScopedTypeVariables #-}
11+{-# LANGUAGE TypeOperators #-}
12+{-# LANGUAGE TypeFamilies #-}
13+#if __GLASGOW_HASKELL__ >= 806
14+{-# LANGUAGE UndecidableInstances #-}
15+#endif
16+module Servant.Swagger.Internal where
17+
18+import Prelude ()
19+import Prelude.Compat
20+
21+import Control.Applicative ((<|>))
22+import Control.Lens
23+import Data.Aeson
24+import Data.HashMap.Strict.InsOrd (InsOrdHashMap)
25+import qualified Data.HashMap.Strict.InsOrd as InsOrdHashMap
26+import Data.Foldable (toList)
27+import Data.Proxy
28+import Data.Typeable
29+import Data.Singletons.Bool
30+import Data.Swagger hiding (Header)
31+import qualified Data.Swagger as Swagger
32+import Data.Swagger.Declare
33+import Data.Text (Text)
34+import qualified Data.Text as Text
35+import GHC.Generics (D1, Meta(..), Rep)
36+import GHC.TypeLits
37+import Network.HTTP.Media (MediaType)
38+import Servant.API
39+import Servant.API.Description (FoldDescription,
40+ reflectDescription)
41+import Servant.API.Generic (ToServantApi, AsApi)
42+import Servant.API.Modifiers (FoldRequired)
43+
44+import Servant.Swagger.Internal.TypeLevel.API
45+
46+-- | Generate a Swagger specification for a servant API.
47+--
48+-- To generate Swagger specification, your data types need
49+-- @'ToParamSchema'@ and/or @'ToSchema'@ instances.
50+--
51+-- @'ToParamSchema'@ is used for @'Capture'@, @'QueryParam'@ and @'Header'@.
52+-- @'ToSchema'@ is used for @'ReqBody'@ and response data types.
53+--
54+-- You can easily derive those instances via @Generic@.
55+-- For more information, refer to <http://hackage.haskell.org/package/swagger2/docs/Data-Swagger.html swagger2 documentation>.
56+--
57+-- Example:
58+--
59+-- @
60+-- newtype Username = Username String deriving (Generic, ToText)
61+--
62+-- instance ToParamSchema Username
63+--
64+-- data User = User
65+-- { username :: Username
66+-- , fullname :: String
67+-- } deriving (Generic)
68+--
69+-- instance ToJSON User
70+-- instance ToSchema User
71+--
72+-- type MyAPI = QueryParam "username" Username :> Get '[JSON] User
73+--
74+-- mySwagger :: Swagger
75+-- mySwagger = toSwagger (Proxy :: Proxy MyAPI)
76+-- @
77+class HasSwagger api where
78+ -- | Generate a Swagger specification for a servant API.
79+ toSwagger :: Proxy api -> Swagger
80+
81+instance HasSwagger Raw where
82+ toSwagger _ = mempty & paths . at "/" ?~ mempty
83+
84+instance HasSwagger EmptyAPI where
85+ toSwagger _ = mempty
86+
87+-- | All operations of sub API.
88+-- This is similar to @'operationsOf'@ but ensures that operations
89+-- indeed belong to the API at compile time.
90+subOperations :: (IsSubAPI sub api, HasSwagger sub) =>
91+ Proxy sub -- ^ Part of a servant API.
92+ -> Proxy api -- ^ The whole servant API.
93+ -> Traversal' Swagger Operation
94+subOperations sub _ = operationsOf (toSwagger sub)
95+
96+-- | Make a singleton Swagger spec (with only one endpoint).
97+-- For endpoints with no content see 'mkEndpointNoContent'.
98+mkEndpoint :: forall a cs hs proxy method status.
99+ (ToSchema a, AllAccept cs, AllToResponseHeader hs, SwaggerMethod method, KnownNat status)
100+ => FilePath -- ^ Endpoint path.
101+ -> proxy (Verb method status cs (Headers hs a)) -- ^ Method, content-types, headers and response.
102+ -> Swagger
103+mkEndpoint path proxy
104+ = mkEndpointWithSchemaRef (Just ref) path proxy
105+ & definitions .~ defs
106+ where
107+ (defs, ref) = runDeclare (declareSchemaRef (Proxy :: Proxy a)) mempty
108+
109+-- | Make a singletone 'Swagger' spec (with only one endpoint) and with no content schema.
110+mkEndpointNoContent :: forall nocontent cs hs proxy method status.
111+ (AllAccept cs, AllToResponseHeader hs, SwaggerMethod method, KnownNat status)
112+ => FilePath -- ^ Endpoint path.
113+ -> proxy (Verb method status cs (Headers hs nocontent)) -- ^ Method, content-types, headers and response.
114+ -> Swagger
115+mkEndpointNoContent path proxy
116+ = mkEndpointWithSchemaRef Nothing path proxy
117+
118+-- | Like @'mkEndpoint'@ but with explicit schema reference.
119+-- Unlike @'mkEndpoint'@ this function does not update @'definitions'@.
120+mkEndpointWithSchemaRef :: forall cs hs proxy method status a.
121+ (AllAccept cs, AllToResponseHeader hs, SwaggerMethod method, KnownNat status)
122+ => Maybe (Referenced Schema)
123+ -> FilePath
124+ -> proxy (Verb method status cs (Headers hs a))
125+ -> Swagger
126+mkEndpointWithSchemaRef mref path _ = mempty
127+ & paths.at path ?~
128+ (mempty & method ?~ (mempty
129+ & produces ?~ MimeList responseContentTypes
130+ & at code ?~ Inline (mempty
131+ & schema .~ mref
132+ & headers .~ responseHeaders)))
133+ where
134+ method = swaggerMethod (Proxy :: Proxy method)
135+ code = fromIntegral (natVal (Proxy :: Proxy status))
136+ responseContentTypes = allContentType (Proxy :: Proxy cs)
137+ responseHeaders = toAllResponseHeaders (Proxy :: Proxy hs)
138+
139+mkEndpointNoContentVerb :: forall proxy method.
140+ (SwaggerMethod method)
141+ => FilePath -- ^ Endpoint path.
142+ -> proxy (NoContentVerb method) -- ^ Method
143+ -> Swagger
144+mkEndpointNoContentVerb path _ = mempty
145+ & paths.at path ?~
146+ (mempty & method ?~ (mempty
147+ & at code ?~ Inline mempty))
148+ where
149+ method = swaggerMethod (Proxy :: Proxy method)
150+ code = 204 -- hardcoded in servant-server
151+
152+-- | Add parameter to every operation in the spec.
153+addParam :: Param -> Swagger -> Swagger
154+addParam param = allOperations.parameters %~ (Inline param :)
155+
156+-- | Add a tag to every operation in the spec.
157+addTag :: Text -> Swagger -> Swagger
158+addTag tag = allOperations.tags %~ ([tag] <>)
159+
160+-- | Add accepted content types to every operation in the spec.
161+addConsumes :: [MediaType] -> Swagger -> Swagger
162+addConsumes cs = allOperations.consumes %~ (<> Just (MimeList cs))
163+
164+-- | Format given text as inline code in Markdown.
165+markdownCode :: Text -> Text
166+markdownCode s = "`" <> s <> "`"
167+
168+addDefaultResponse400 :: ParamName -> Swagger -> Swagger
169+addDefaultResponse400 pname = setResponseWith (\old _new -> alter400 old) 400 (return response400)
170+ where
171+ sname = markdownCode pname
172+ description400 = "Invalid " <> sname
173+ alter400 = description %~ (<> (" or " <> sname))
174+ response400 = mempty & description .~ description400
175+
176+-- | Methods, available for Swagger.
177+class SwaggerMethod method where
178+ swaggerMethod :: proxy method -> Lens' PathItem (Maybe Operation)
179+
180+instance SwaggerMethod 'GET where swaggerMethod _ = get
181+instance SwaggerMethod 'PUT where swaggerMethod _ = put
182+instance SwaggerMethod 'POST where swaggerMethod _ = post
183+instance SwaggerMethod 'DELETE where swaggerMethod _ = delete
184+instance SwaggerMethod 'OPTIONS where swaggerMethod _ = options
185+instance SwaggerMethod 'HEAD where swaggerMethod _ = head_
186+instance SwaggerMethod 'PATCH where swaggerMethod _ = patch
187+
188+instance HasSwagger (UVerb method cs '[]) where
189+ toSwagger _ = mempty
190+
191+-- | @since <TODO>
192+instance
193+ {-# OVERLAPPABLE #-}
194+ ( ToSchema a,
195+ HasStatus a,
196+ AllAccept cs,
197+ SwaggerMethod method,
198+ HasSwagger (UVerb method cs as)
199+ ) =>
200+ HasSwagger (UVerb method cs (a ': as))
201+ where
202+ toSwagger _ =
203+ toSwagger (Proxy :: Proxy (Verb method (StatusOf a) cs a))
204+ `combineSwagger` toSwagger (Proxy :: Proxy (UVerb method cs as))
205+
206+-- ATTENTION: do not remove this instance!
207+-- A similar instance above will always use the more general
208+-- polymorphic -- HasSwagger instance and will result in a type error
209+-- since 'NoContent' does not have a 'ToSchema' instance.
210+instance
211+ ( KnownNat status,
212+ AllAccept cs,
213+ SwaggerMethod method,
214+ HasSwagger (UVerb method cs as)
215+ ) =>
216+ HasSwagger (UVerb method cs (WithStatus status NoContent ': as))
217+ where
218+ toSwagger _ =
219+ toSwagger (Proxy :: Proxy (Verb method status cs NoContent))
220+ `combineSwagger` toSwagger (Proxy :: Proxy (UVerb method cs as))
221+
222+
223+-- workaround for https://github.com/GetShopTV/swagger2/issues/218
224+-- We'd like to juse use (<>) but the instances are wrong
225+combinePathItem :: PathItem -> PathItem -> PathItem
226+combinePathItem s t = PathItem
227+ { _pathItemGet = _pathItemGet s <> _pathItemGet t
228+ , _pathItemPut = _pathItemPut s <> _pathItemPut t
229+ , _pathItemPost = _pathItemPost s <> _pathItemPost t
230+ , _pathItemDelete = _pathItemDelete s <> _pathItemDelete t
231+ , _pathItemOptions = _pathItemOptions s <> _pathItemOptions t
232+ , _pathItemHead = _pathItemHead s <> _pathItemHead t
233+ , _pathItemPatch = _pathItemPatch s <> _pathItemPatch t
234+ , _pathItemParameters = _pathItemParameters s <> _pathItemParameters t
235+ }
236+
237+combineSwagger :: Swagger -> Swagger -> Swagger
238+combineSwagger s t = Swagger
239+ { _swaggerInfo = _swaggerInfo s <> _swaggerInfo t
240+ , _swaggerHost = _swaggerHost s <|> _swaggerHost t
241+ , _swaggerBasePath = _swaggerBasePath s <|> _swaggerBasePath t
242+ , _swaggerSchemes = _swaggerSchemes s <> _swaggerSchemes t
243+ , _swaggerConsumes = _swaggerConsumes s <> _swaggerConsumes t
244+ , _swaggerProduces = _swaggerProduces s <> _swaggerProduces t
245+ , _swaggerPaths = InsOrdHashMap.unionWith combinePathItem (_swaggerPaths s) (_swaggerPaths t)
246+ , _swaggerDefinitions = _swaggerDefinitions s <> _swaggerDefinitions t
247+ , _swaggerParameters = _swaggerParameters s <> _swaggerParameters t
248+ , _swaggerResponses = _swaggerResponses s <> _swaggerResponses t
249+ , _swaggerSecurityDefinitions = _swaggerSecurityDefinitions s <> _swaggerSecurityDefinitions t
250+ , _swaggerSecurity = _swaggerSecurity s <> _swaggerSecurity t
251+ , _swaggerTags = _swaggerTags s <> _swaggerTags t
252+ , _swaggerExternalDocs = _swaggerExternalDocs s <|> _swaggerExternalDocs t
253+ }
254+
255+instance {-# OVERLAPPABLE #-} (ToSchema a, AllAccept cs, KnownNat status, SwaggerMethod method) => HasSwagger (Verb method status cs a) where
256+ toSwagger _ = toSwagger (Proxy :: Proxy (Verb method status cs (Headers '[] a)))
257+
258+-- | @since 1.1.7
259+instance (ToSchema a, Accept ct, KnownNat status, SwaggerMethod method) => HasSwagger (Stream method status fr ct a) where
260+ toSwagger _ = toSwagger (Proxy :: Proxy (Verb method status '[ct] (Headers '[] a)))
261+
262+instance {-# OVERLAPPABLE #-} (ToSchema a, AllAccept cs, AllToResponseHeader hs, KnownNat status, SwaggerMethod method)
263+ => HasSwagger (Verb method status cs (Headers hs a)) where
264+ toSwagger = mkEndpoint "/"
265+
266+-- ATTENTION: do not remove this instance!
267+-- A similar instance above will always use the more general
268+-- polymorphic -- HasSwagger instance and will result in a type error
269+-- since 'NoContent' does not have a 'ToSchema' instance.
270+instance (AllAccept cs, KnownNat status, SwaggerMethod method) => HasSwagger (Verb method status cs NoContent) where
271+ toSwagger _ = toSwagger (Proxy :: Proxy (Verb method status cs (Headers '[] NoContent)))
272+
273+instance (AllAccept cs, AllToResponseHeader hs, KnownNat status, SwaggerMethod method)
274+ => HasSwagger (Verb method status cs (Headers hs NoContent)) where
275+ toSwagger = mkEndpointNoContent "/"
276+
277+instance (SwaggerMethod method) => HasSwagger (NoContentVerb method) where
278+ toSwagger = mkEndpointNoContentVerb "/"
279+
280+instance (HasSwagger a, HasSwagger b) => HasSwagger (a :<|> b) where
281+ toSwagger _ = toSwagger (Proxy :: Proxy a) <> toSwagger (Proxy :: Proxy b)
282+
283+-- | @'Vault'@ combinator does not change our specification at all.
284+instance (HasSwagger sub) => HasSwagger (Vault :> sub) where
285+ toSwagger _ = toSwagger (Proxy :: Proxy sub)
286+
287+-- | @'IsSecure'@ combinator does not change our specification at all.
288+instance (HasSwagger sub) => HasSwagger (IsSecure :> sub) where
289+ toSwagger _ = toSwagger (Proxy :: Proxy sub)
290+
291+-- | @'RemoteHost'@ combinator does not change our specification at all.
292+instance (HasSwagger sub) => HasSwagger (RemoteHost :> sub) where
293+ toSwagger _ = toSwagger (Proxy :: Proxy sub)
294+
295+-- | @'Fragment'@ combinator does not change our specification at all.
296+instance HasSwagger sub => HasSwagger (Fragment a :> sub) where
297+ toSwagger _ = toSwagger (Proxy :: Proxy sub)
298+
299+-- | @'HttpVersion'@ combinator does not change our specification at all.
300+instance (HasSwagger sub) => HasSwagger (HttpVersion :> sub) where
301+ toSwagger _ = toSwagger (Proxy :: Proxy sub)
302+
303+-- | @'WithNamedContext'@ combinator does not change our specification at all.
304+instance (HasSwagger sub) => HasSwagger (WithNamedContext x c sub) where
305+ toSwagger _ = toSwagger (Proxy :: Proxy sub)
306+
307+instance (KnownSymbol sym, HasSwagger sub) => HasSwagger (sym :> sub) where
308+ toSwagger _ = prependPath piece (toSwagger (Proxy :: Proxy sub))
309+ where
310+ piece = symbolVal (Proxy :: Proxy sym)
311+
312+instance (KnownSymbol sym, Typeable a, ToParamSchema a, HasSwagger sub, KnownSymbol (FoldDescription mods)) => HasSwagger (Capture' mods sym a :> sub) where
313+ toSwagger _ = toSwagger (Proxy :: Proxy sub)
314+ & addParam param
315+ & prependPath capture
316+ & addDefaultResponse400 tname
317+ where
318+ symbol = symbolVal (Proxy :: Proxy sym)
319+ pname = if symbol == ""
320+ then camelTo2 '-' . tyConName . typeRepTyCon $ typeRep (Proxy :: Proxy a)
321+ else symbol
322+ tname = Text.pack pname
323+ transDesc "" = Nothing
324+ transDesc desc = Just (Text.pack desc)
325+ capture = "{" <> pname <> "}"
326+ param = mempty
327+ & name .~ tname
328+ & description .~ transDesc (reflectDescription (Proxy :: Proxy mods))
329+ & required ?~ True
330+ & schema .~ ParamOther (mempty
331+ & in_ .~ ParamPath
332+ & paramSchema .~ toParamSchema (Proxy :: Proxy a))
333+
334+-- | Swagger Spec doesn't have a notion of CaptureAll, this instance is the best effort.
335+instance (KnownSymbol sym, Typeable a, ToParamSchema a, HasSwagger sub) => HasSwagger (CaptureAll sym a :> sub) where
336+ toSwagger _ = toSwagger (Proxy :: Proxy (Capture sym a :> sub))
337+
338+instance (KnownSymbol desc, HasSwagger api) => HasSwagger (Description desc :> api) where
339+ toSwagger _ = toSwagger (Proxy :: Proxy api)
340+ & allOperations.description %~ (Just (Text.pack (symbolVal (Proxy :: Proxy desc))) <>)
341+
342+instance (KnownSymbol desc, HasSwagger api) => HasSwagger (Summary desc :> api) where
343+ toSwagger _ = toSwagger (Proxy :: Proxy api)
344+ & allOperations.summary %~ (Just (Text.pack (symbolVal (Proxy :: Proxy desc))) <>)
345+
346+instance (KnownSymbol sym, ToParamSchema a, HasSwagger sub, SBoolI (FoldRequired mods), KnownSymbol (FoldDescription mods)) => HasSwagger (QueryParam' mods sym a :> sub) where
347+ toSwagger _ = toSwagger (Proxy :: Proxy sub)
348+ & addParam param
349+ & addDefaultResponse400 tname
350+ where
351+ tname = Text.pack (symbolVal (Proxy :: Proxy sym))
352+ transDesc "" = Nothing
353+ transDesc desc = Just (Text.pack desc)
354+ param = mempty
355+ & name .~ tname
356+ & description .~ transDesc (reflectDescription (Proxy :: Proxy mods))
357+ & required ?~ reflectBool (Proxy :: Proxy (FoldRequired mods))
358+ & schema .~ ParamOther sch
359+ sch = mempty
360+ & in_ .~ ParamQuery
361+ & paramSchema .~ toParamSchema (Proxy :: Proxy a)
362+
363+instance (KnownSymbol sym, ToParamSchema a, HasSwagger sub) => HasSwagger (QueryParams sym a :> sub) where
364+ toSwagger _ = toSwagger (Proxy :: Proxy sub)
365+ & addParam param
366+ & addDefaultResponse400 tname
367+ where
368+ tname = Text.pack (symbolVal (Proxy :: Proxy sym))
369+ param = mempty
370+ & name .~ tname
371+ & schema .~ ParamOther sch
372+ sch = mempty
373+ & in_ .~ ParamQuery
374+ & paramSchema .~ pschema
375+ pschema = mempty
376+#if MIN_VERSION_swagger2(2,4,0)
377+ & type_ ?~ SwaggerArray
378+#else
379+ & type_ .~ SwaggerArray
380+#endif
381+ & items ?~ SwaggerItemsPrimitive (Just CollectionMulti) (toParamSchema (Proxy :: Proxy a))
382+
383+instance (KnownSymbol sym, HasSwagger sub) => HasSwagger (QueryFlag sym :> sub) where
384+ toSwagger _ = toSwagger (Proxy :: Proxy sub)
385+ & addParam param
386+ & addDefaultResponse400 tname
387+ where
388+ tname = Text.pack (symbolVal (Proxy :: Proxy sym))
389+ param = mempty
390+ & name .~ tname
391+ & schema .~ ParamOther (mempty
392+ & in_ .~ ParamQuery
393+ & allowEmptyValue ?~ True
394+ & paramSchema .~ (toParamSchema (Proxy :: Proxy Bool)
395+ & default_ ?~ toJSON False))
396+
397+instance (KnownSymbol sym, ToParamSchema a, HasSwagger sub, SBoolI (FoldRequired mods), KnownSymbol (FoldDescription mods)) => HasSwagger (Header' mods sym a :> sub) where
398+ toSwagger _ = toSwagger (Proxy :: Proxy sub)
399+ & addParam param
400+ & addDefaultResponse400 tname
401+ where
402+ tname = Text.pack (symbolVal (Proxy :: Proxy sym))
403+ transDesc "" = Nothing
404+ transDesc desc = Just (Text.pack desc)
405+ param = mempty
406+ & name .~ tname
407+ & description .~ transDesc (reflectDescription (Proxy :: Proxy mods))
408+ & required ?~ reflectBool (Proxy :: Proxy (FoldRequired mods))
409+ & schema .~ ParamOther (mempty
410+ & in_ .~ ParamHeader
411+ & paramSchema .~ toParamSchema (Proxy :: Proxy a))
412+
413+instance (ToSchema a, AllAccept cs, HasSwagger sub, KnownSymbol (FoldDescription mods)) => HasSwagger (ReqBody' mods cs a :> sub) where
414+ toSwagger _ = toSwagger (Proxy :: Proxy sub)
415+ & addParam param
416+ & addConsumes (allContentType (Proxy :: Proxy cs))
417+ & addDefaultResponse400 tname
418+ & definitions %~ (<> defs)
419+ where
420+ tname = "body"
421+ transDesc "" = Nothing
422+ transDesc desc = Just (Text.pack desc)
423+ (defs, ref) = runDeclare (declareSchemaRef (Proxy :: Proxy a)) mempty
424+ param = mempty
425+ & name .~ tname
426+ & description .~ transDesc (reflectDescription (Proxy :: Proxy mods))
427+ & required ?~ True
428+ & schema .~ ParamBody ref
429+
430+-- | This instance is an approximation.
431+--
432+-- @since 1.1.7
433+instance (ToSchema a, Accept ct, HasSwagger sub, KnownSymbol (FoldDescription mods)) => HasSwagger (StreamBody' mods fr ct a :> sub) where
434+ toSwagger _ = toSwagger (Proxy :: Proxy sub)
435+ & addParam param
436+ & addConsumes (toList (contentTypes (Proxy :: Proxy ct)))
437+ & addDefaultResponse400 tname
438+ & definitions %~ (<> defs)
439+ where
440+ tname = "body"
441+ transDesc "" = Nothing
442+ transDesc desc = Just (Text.pack desc)
443+ (defs, ref) = runDeclare (declareSchemaRef (Proxy :: Proxy a)) mempty
444+ param = mempty
445+ & name .~ tname
446+ & description .~ transDesc (reflectDescription (Proxy :: Proxy mods))
447+ & required ?~ True
448+ & schema .~ ParamBody ref
449+
450+instance (HasSwagger (ToServantApi routes), KnownSymbol datatypeName, Rep (routes AsApi) ~ D1 ('MetaData datatypeName moduleName packageName isNewtype) f) => HasSwagger (NamedRoutes routes) where
451+ toSwagger _ = addTag (Text.pack $ symbolVal (Proxy :: Proxy datatypeName)) (toSwagger (Proxy :: Proxy (ToServantApi routes)))
452+
453+-- =======================================================================
454+-- Below are the definitions that should be in Servant.API.ContentTypes
455+-- =======================================================================
456+
457+class AllAccept cs where
458+ allContentType :: Proxy cs -> [MediaType]
459+
460+instance AllAccept '[] where
461+ allContentType _ = []
462+
463+instance (Accept c, AllAccept cs) => AllAccept (c ': cs) where
464+ allContentType _ = contentType (Proxy :: Proxy c) : allContentType (Proxy :: Proxy cs)
465+
466+class ToResponseHeader h where
467+ toResponseHeader :: Proxy h -> (HeaderName, Swagger.Header)
468+
469+instance (KnownSymbol sym, ToParamSchema a) => ToResponseHeader (Header sym a) where
470+ toResponseHeader _ = (hname, Swagger.Header Nothing hschema)
471+ where
472+ hname = Text.pack (symbolVal (Proxy :: Proxy sym))
473+ hschema = toParamSchema (Proxy :: Proxy a)
474+
475+class AllToResponseHeader hs where
476+ toAllResponseHeaders :: Proxy hs -> InsOrdHashMap HeaderName Swagger.Header
477+
478+instance AllToResponseHeader '[] where
479+ toAllResponseHeaders _ = mempty
480+
481+instance (ToResponseHeader h, AllToResponseHeader hs) => AllToResponseHeader (h ': hs) where
482+ toAllResponseHeaders _ = InsOrdHashMap.insert headerName headerBS hdrs
483+ where
484+ (headerName, headerBS) = toResponseHeader (Proxy :: Proxy h)
485+ hdrs = toAllResponseHeaders (Proxy :: Proxy hs)
486+
487+instance AllToResponseHeader hs => AllToResponseHeader (HList hs) where
488+ toAllResponseHeaders _ = toAllResponseHeaders (Proxy :: Proxy hs)
servant-swagger/src/Servant/Swagger/Internal/Orphans.hsnew file+27
@@ -0,0 +1,27 @@
1+{-# OPTIONS_GHC -fno-warn-orphans #-}
2+{-# LANGUAGE CPP #-}
3+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4+{-# LANGUAGE ScopedTypeVariables #-}
5+{-# LANGUAGE StandaloneDeriving #-}
6+module Servant.Swagger.Internal.Orphans where
7+
8+import Data.Proxy
9+ (Proxy (..))
10+import Data.Swagger
11+import Servant.Types.SourceT
12+ (SourceT)
13+#if MIN_VERSION_GLASGOW_HASKELL(8,8,1,0)
14+import Servant.API (WithStatus(..))
15+#endif
16+
17+-- | Pretend that 'SourceT m a' is '[a]'.
18+--
19+-- @since 1.1.7
20+--
21+instance ToSchema a => ToSchema (SourceT m a) where
22+ declareNamedSchema _ = declareNamedSchema (Proxy :: Proxy [a])
23+
24+#if MIN_VERSION_GLASGOW_HASKELL(8,8,1,0)
25+-- @since 1.1.11
26+deriving instance ToSchema a => ToSchema (WithStatus s a)
27+#endif
servant-swagger/src/Servant/Swagger/Internal/Test.hsnew file+207
@@ -0,0 +1,207 @@
1+{-# LANGUAGE ConstraintKinds #-}
2+{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE FlexibleContexts #-}
4+{-# LANGUAGE RankNTypes #-}
5+{-# LANGUAGE ScopedTypeVariables #-}
6+{-# LANGUAGE TypeOperators #-}
7+{-# LANGUAGE OverloadedStrings #-}
8+module Servant.Swagger.Internal.Test where
9+
10+import Data.Aeson (ToJSON (..))
11+import Data.Aeson.Encode.Pretty (encodePretty', defConfig,
12+ confCompare)
13+import Data.Swagger (Pattern, ToSchema,
14+ toSchema)
15+import Data.Swagger.Schema.Validation
16+import Data.Text (Text)
17+import qualified Data.Text.Lazy as TL
18+import qualified Data.Text.Lazy.Encoding as TL
19+import Data.Typeable
20+import Test.Hspec
21+import Test.Hspec.QuickCheck
22+import Test.QuickCheck (Arbitrary, Property,
23+ counterexample, property)
24+
25+import Servant.API
26+import Servant.Swagger.Internal.TypeLevel
27+
28+-- $setup
29+-- >>> import Control.Applicative
30+-- >>> import GHC.Generics
31+-- >>> import Test.QuickCheck
32+-- >>> import System.Environment (setEnv)
33+-- >>> :set -XDeriveGeneric
34+-- >>> :set -XGeneralizedNewtypeDeriving
35+-- >>> :set -XDataKinds
36+-- >>> :set -XTypeOperators
37+-- >>> setEnv "HSPEC_COLOR" "no"
38+
39+-- | Verify that every type used with @'JSON'@ content type in a servant API
40+-- has compatible @'ToJSON'@ and @'ToSchema'@ instances using @'validateToJSON'@.
41+--
42+-- /NOTE:/ @'validateEveryToJSON'@ does not perform string pattern validation.
43+-- See @'validateEveryToJSONWithPatternChecker'@.
44+--
45+-- @'validateEveryToJSON'@ will produce one @'prop'@ specification for every type in the API.
46+-- Each type only gets one test, even if it occurs multiple times in the API.
47+--
48+-- >>> data User = User { name :: String, age :: Maybe Int } deriving (Show, Generic, Typeable)
49+-- >>> newtype UserId = UserId String deriving (Show, Generic, Typeable, ToJSON, Arbitrary)
50+-- >>> instance ToJSON User
51+-- >>> instance ToSchema User
52+-- >>> instance ToSchema UserId
53+-- >>> instance Arbitrary User where arbitrary = User <$> arbitrary <*> arbitrary
54+-- >>> type UserAPI = (Capture "user_id" UserId :> Get '[JSON] User) :<|> (ReqBody '[JSON] User :> Post '[JSON] UserId)
55+--
56+-- >>> hspec $ context "ToJSON matches ToSchema" $ validateEveryToJSON (Proxy :: Proxy UserAPI)
57+-- <BLANKLINE>
58+-- ToJSON matches ToSchema...
59+-- User...
60+-- ...
61+-- UserId...
62+-- ...
63+-- Finished in ... seconds
64+-- 2 examples, 0 failures
65+--
66+-- For the test to compile all body types should have the following instances:
67+--
68+-- * @'ToJSON'@ and @'ToSchema'@ are used to perform the validation;
69+-- * @'Typeable'@ is used to name the test for each type;
70+-- * @'Show'@ is used to display value for which @'ToJSON'@ does not satisfy @'ToSchema'@.
71+-- * @'Arbitrary'@ is used to arbitrarily generate values.
72+--
73+-- If any of the instances is missing, you'll get a descriptive type error:
74+--
75+-- >>> data Contact = Contact { fullname :: String, phone :: Integer } deriving (Show, Generic)
76+-- >>> instance ToJSON Contact
77+-- >>> instance ToSchema Contact
78+-- >>> type ContactAPI = Get '[JSON] Contact
79+-- >>> hspec $ validateEveryToJSON (Proxy :: Proxy ContactAPI)
80+-- ...
81+-- ...No instance for (Arbitrary Contact)
82+-- ... arising from a use of ‘validateEveryToJSON’
83+-- ...
84+validateEveryToJSON
85+ :: forall proxy api .
86+ TMap (Every [Typeable, Show, Arbitrary, ToJSON, ToSchema])
87+ (BodyTypes JSON api)
88+ => proxy api -- ^ Servant API.
89+ -> Spec
90+validateEveryToJSON _ = props
91+ (Proxy :: Proxy [ToJSON, ToSchema])
92+ (maybeCounterExample . prettyValidateWith validateToJSON)
93+ (Proxy :: Proxy (BodyTypes JSON api))
94+
95+-- | Verify that every type used with @'JSON'@ content type in a servant API
96+-- has compatible @'ToJSON'@ and @'ToSchema'@ instances using @'validateToJSONWithPatternChecker'@.
97+--
98+-- For validation without patterns see @'validateEveryToJSON'@.
99+validateEveryToJSONWithPatternChecker :: forall proxy api. TMap (Every [Typeable, Show, Arbitrary, ToJSON, ToSchema]) (BodyTypes JSON api) =>
100+ (Pattern -> Text -> Bool) -- ^ @'Pattern'@ checker.
101+ -> proxy api -- ^ Servant API.
102+ -> Spec
103+validateEveryToJSONWithPatternChecker checker _ = props
104+ (Proxy :: Proxy [ToJSON, ToSchema])
105+ (maybeCounterExample . prettyValidateWith (validateToJSONWithPatternChecker checker))
106+ (Proxy :: Proxy (BodyTypes JSON api))
107+
108+-- * QuickCheck-related stuff
109+
110+-- | Construct property tests for each type in a list.
111+-- The name for each property is the name of the corresponding type.
112+--
113+-- >>> :{
114+-- hspec $
115+-- context "read . show == id" $
116+-- props
117+-- (Proxy :: Proxy [Eq, Show, Read])
118+-- (\x -> read (show x) === x)
119+-- (Proxy :: Proxy [Bool, Int, String])
120+-- :}
121+-- <BLANKLINE>
122+-- read . show == id
123+-- Bool...
124+-- ...
125+-- Int...
126+-- ...
127+-- [Char]...
128+-- ...
129+-- Finished in ... seconds
130+-- 3 examples, 0 failures
131+props :: forall p p'' cs xs. TMap (Every (Typeable ': Show ': Arbitrary ': cs)) xs =>
132+ p cs -- ^ A list of constraints.
133+ -> (forall x. EveryTF cs x => x -> Property) -- ^ Property predicate.
134+ -> p'' xs -- ^ A list of types.
135+ -> Spec
136+props _ f px = sequence_ specs
137+ where
138+ specs :: [Spec]
139+ specs = tmapEvery (Proxy :: Proxy (Typeable ': Show ': Arbitrary ': cs)) aprop px
140+
141+ aprop :: forall p' a. (EveryTF cs a, Typeable a, Show a, Arbitrary a) => p' a -> Spec
142+ aprop _ = prop (show (typeOf (undefined :: a))) (f :: a -> Property)
143+
144+-- | Pretty print validation errors
145+-- together with actual JSON and Swagger Schema
146+-- (using 'encodePretty').
147+--
148+-- >>> import Data.Aeson
149+-- >>> import Data.Foldable (traverse_)
150+-- >>> data Person = Person { name :: String, phone :: Integer } deriving (Generic)
151+-- >>> instance ToJSON Person where toJSON p = object [ "name" .= name p ]
152+-- >>> instance ToSchema Person
153+-- >>> let person = Person { name = "John", phone = 123456 }
154+-- >>> traverse_ putStrLn $ prettyValidateWith validateToJSON person
155+-- Validation against the schema fails:
156+-- * property "phone" is required, but not found in "{\"name\":\"John\"}"
157+-- <BLANKLINE>
158+-- JSON value:
159+-- {
160+-- "name": "John"
161+-- }
162+-- <BLANKLINE>
163+-- Swagger Schema:
164+-- {
165+-- "properties": {
166+-- "name": {
167+-- "type": "string"
168+-- },
169+-- "phone": {
170+-- "type": "integer"
171+-- }
172+-- },
173+-- "required": [
174+-- "name",
175+-- "phone"
176+-- ],
177+-- "type": "object"
178+-- }
179+-- <BLANKLINE>
180+--
181+-- FIXME: this belongs in "Data.Swagger.Schema.Validation" (in @swagger2@).
182+prettyValidateWith
183+ :: forall a. (ToJSON a, ToSchema a)
184+ => (a -> [ValidationError]) -> a -> Maybe String
185+prettyValidateWith f x =
186+ case f x of
187+ [] -> Nothing
188+ errors -> Just $ unlines
189+ [ "Validation against the schema fails:"
190+ , unlines (map (" * " ++) errors)
191+ , "JSON value:"
192+ , ppJSONString json
193+ , ""
194+ , "Swagger Schema:"
195+ , ppJSONString (toJSON schema)
196+ ]
197+ where
198+ ppJSONString = TL.unpack . TL.decodeUtf8 . encodePretty' ppCfg
199+ ppCfg = defConfig { confCompare = compare }
200+
201+ json = toJSON x
202+ schema = toSchema (Proxy :: Proxy a)
203+
204+-- | Provide a counterexample if there is any.
205+maybeCounterExample :: Maybe String -> Property
206+maybeCounterExample Nothing = property True
207+maybeCounterExample (Just s) = counterexample s (property False)
servant-swagger/src/Servant/Swagger/Internal/TypeLevel.hsnew file+9
@@ -0,0 +1,9 @@
1+module Servant.Swagger.Internal.TypeLevel (
2+ module Servant.Swagger.Internal.TypeLevel.API,
3+ module Servant.Swagger.Internal.TypeLevel.Every,
4+ module Servant.Swagger.Internal.TypeLevel.TMap,
5+) where
6+
7+import Servant.Swagger.Internal.TypeLevel.API
8+import Servant.Swagger.Internal.TypeLevel.Every
9+import Servant.Swagger.Internal.TypeLevel.TMap
servant-swagger/src/Servant/Swagger/Internal/TypeLevel/API.hsnew file+86
@@ -0,0 +1,86 @@
1+{-# LANGUAGE ConstraintKinds #-}
2+{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE KindSignatures #-}
4+{-# LANGUAGE PolyKinds #-}
5+{-# LANGUAGE TypeFamilies #-}
6+{-# LANGUAGE TypeOperators #-}
7+{-# LANGUAGE UndecidableInstances #-}
8+module Servant.Swagger.Internal.TypeLevel.API where
9+
10+import GHC.Exts (Constraint)
11+import Servant.API
12+
13+-- | Build a list of endpoints from an API.
14+type family EndpointsList api where
15+ EndpointsList (a :<|> b) = AppendList (EndpointsList a) (EndpointsList b)
16+ EndpointsList (e :> a) = MapSub e (EndpointsList a)
17+ EndpointsList a = '[a]
18+
19+-- | Check whether @sub@ is a sub API of @api@.
20+type family IsSubAPI sub api :: Constraint where
21+ IsSubAPI sub api = AllIsElem (EndpointsList sub) api
22+
23+-- | Check that every element of @xs@ is an endpoint of @api@.
24+type family AllIsElem xs api :: Constraint where
25+ AllIsElem '[] api = ()
26+ AllIsElem (x ': xs) api = (IsIn x api, AllIsElem xs api)
27+
28+-- | Apply @(e :>)@ to every API in @xs@.
29+type family MapSub e xs where
30+ MapSub e '[] = '[]
31+ MapSub e (x ': xs) = (e :> x) ': MapSub e xs
32+
33+-- | Append two type-level lists.
34+type family AppendList xs ys where
35+ AppendList '[] ys = ys
36+ AppendList (x ': xs) ys = x ': AppendList xs ys
37+
38+type family Or (a :: Constraint) (b :: Constraint) :: Constraint where
39+ Or () b = ()
40+ Or a () = ()
41+
42+type family IsIn sub api :: Constraint where
43+ IsIn e (a :<|> b) = Or (IsIn e a) (IsIn e b)
44+ IsIn (e :> a) (e :> b) = IsIn a b
45+ IsIn e e = ()
46+
47+-- | Check whether a type is a member of a list of types.
48+-- This is a type-level analogue of @'elem'@.
49+type family Elem x xs where
50+ Elem x '[] = 'False
51+ Elem x (x ': xs) = 'True
52+ Elem x (y ': xs) = Elem x xs
53+
54+-- | Remove duplicates from a type-level list.
55+type family Nub xs where
56+ Nub '[] = '[]
57+ Nub (x ': xs) = x ': Nub (Remove x xs)
58+
59+-- | Remove element from a type-level list.
60+type family Remove x xs where
61+ Remove x '[] = '[]
62+ Remove x (x ': ys) = Remove x ys
63+ Remove x (y ': ys) = y ': Remove x ys
64+
65+-- | Extract a list of unique "body" types for a specific content-type from a servant API.
66+type BodyTypes c api = Nub (BodyTypes' c api)
67+
68+-- | @'AddBodyType' c cs a as@ adds type @a@ to the list @as@
69+-- only if @c@ is in @cs@.
70+type AddBodyType c cs a as = If (Elem c cs) (a ': as) as
71+
72+-- | Extract a list of "body" types for a specific content-type from a servant API.
73+-- To extract unique types see @'BodyTypes'@.
74+--
75+-- @'NoContent'@ is removed from the list and not tested. (This allows for leaving the body
76+-- completely empty on responses to requests that only accept 'application/json', while
77+-- setting the content-type in the response accordingly.)
78+type family BodyTypes' c api :: [*] where
79+ BodyTypes' c (Verb verb b cs (Headers hdrs a)) = AddBodyType c cs a '[]
80+ BodyTypes' c (Verb verb b cs NoContent) = '[]
81+ BodyTypes' c (Verb verb b cs a) = AddBodyType c cs a '[]
82+ BodyTypes' c (ReqBody' mods cs a :> api) = AddBodyType c cs a (BodyTypes' c api)
83+ BodyTypes' c (e :> api) = BodyTypes' c api
84+ BodyTypes' c (a :<|> b) = AppendList (BodyTypes' c a) (BodyTypes' c b)
85+ BodyTypes' c api = '[]
86+
servant-swagger/src/Servant/Swagger/Internal/TypeLevel/Every.hsnew file+63
@@ -0,0 +1,63 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE ConstraintKinds #-}
3+{-# LANGUAGE DataKinds #-}
4+{-# LANGUAGE FlexibleContexts #-}
5+{-# LANGUAGE FlexibleInstances #-}
6+{-# LANGUAGE GADTs #-}
7+{-# LANGUAGE InstanceSigs #-}
8+{-# LANGUAGE KindSignatures #-}
9+{-# LANGUAGE MultiParamTypeClasses #-}
10+{-# LANGUAGE PolyKinds #-}
11+{-# LANGUAGE RankNTypes #-}
12+{-# LANGUAGE ScopedTypeVariables #-}
13+{-# LANGUAGE TypeFamilies #-}
14+{-# LANGUAGE TypeOperators #-}
15+{-# LANGUAGE UndecidableInstances #-}
16+#if __GLASGOW_HASKELL__ >= 800
17+{-# LANGUAGE UndecidableSuperClasses #-}
18+#endif
19+module Servant.Swagger.Internal.TypeLevel.Every where
20+
21+import Data.Proxy
22+import GHC.Exts (Constraint)
23+
24+import Servant.Swagger.Internal.TypeLevel.TMap
25+
26+-- $setup
27+-- >>> :set -XDataKinds
28+-- >>> :set -XFlexibleContexts
29+-- >>> :set -XGADTs
30+-- >>> :set -XRankNTypes
31+-- >>> :set -XScopedTypeVariables
32+-- >>> import GHC.TypeLits
33+-- >>> import Data.List
34+
35+-- | Apply multiple constraint constructors to a type.
36+--
37+-- @
38+-- EveryTF '[Show, Read] a ~ (Show a, Read a)
39+-- @
40+--
41+-- Note that since this is a type family, you have to alway fully apply @'EveryTF'@.
42+--
43+-- For partial application of multiple constraint constructors see @'Every'@.
44+type family EveryTF cs x :: Constraint where
45+ EveryTF '[] x = ()
46+ EveryTF (c ': cs) x = (c x, EveryTF cs x)
47+
48+-- | Apply multiple constraint constructors to a type as a class.
49+--
50+-- This is different from @'EveryTF'@ in that it allows partial application.
51+class EveryTF cs x => Every (cs :: [* -> Constraint]) (x :: *) where
52+
53+instance Every '[] x where
54+instance (c x, Every cs x) => Every (c ': cs) x where
55+
56+-- | Like @'tmap'@, but uses @'Every'@ for multiple constraints.
57+--
58+-- >>> let zero :: forall p a. (Show a, Num a) => p a -> String; zero _ = show (0 :: a)
59+-- >>> tmapEvery (Proxy :: Proxy [Show, Num]) zero (Proxy :: Proxy [Int, Float]) :: [String]
60+-- ["0","0.0"]
61+tmapEvery :: forall a cs p p'' xs. (TMap (Every cs) xs) =>
62+ p cs -> (forall x p'. Every cs x => p' x -> a) -> p'' xs -> [a]
63+tmapEvery _ = tmap (Proxy :: Proxy (Every cs))
servant-swagger/src/Servant/Swagger/Internal/TypeLevel/TMap.hsnew file+37
@@ -0,0 +1,37 @@
1+{-# LANGUAGE ConstraintKinds #-}
2+{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE FlexibleInstances #-}
4+{-# LANGUAGE KindSignatures #-}
5+{-# LANGUAGE MultiParamTypeClasses #-}
6+{-# LANGUAGE PolyKinds #-}
7+{-# LANGUAGE RankNTypes #-}
8+{-# LANGUAGE ScopedTypeVariables #-}
9+{-# LANGUAGE TypeOperators #-}
10+{-# LANGUAGE UndecidableInstances #-}
11+module Servant.Swagger.Internal.TypeLevel.TMap where
12+
13+import Data.Proxy
14+import GHC.Exts (Constraint)
15+
16+-- $setup
17+-- >>> :set -XDataKinds
18+-- >>> :set -XFlexibleContexts
19+-- >>> :set -XGADTs
20+-- >>> :set -XRankNTypes
21+-- >>> :set -XScopedTypeVariables
22+-- >>> import GHC.TypeLits
23+-- >>> import Data.List
24+
25+-- | Map a list of constrained types to a list of values.
26+--
27+-- >>> tmap (Proxy :: Proxy KnownSymbol) symbolVal (Proxy :: Proxy ["hello", "world"])
28+-- ["hello","world"]
29+class TMap (q :: k -> Constraint) (xs :: [k]) where
30+ tmap :: p q -> (forall x p'. q x => p' x -> a) -> p'' xs -> [a]
31+
32+instance TMap q '[] where
33+ tmap _ _ _ = []
34+
35+instance (q x, TMap q xs) => TMap q (x ': xs) where
36+ tmap q f _ = f (Proxy :: Proxy x) : tmap q f (Proxy :: Proxy xs)
37+
servant-swagger/src/Servant/Swagger/Test.hsnew file+13
@@ -0,0 +1,13 @@
1+-- |
2+-- Module: Servant.Swagger.Test
3+-- License: BSD3
4+-- Maintainer: Nickolay Kudasov <nickolay@getshoptv.com>
5+-- Stability: experimental
6+--
7+-- Automatic tests for servant API against Swagger spec.
8+module Servant.Swagger.Test (
9+ validateEveryToJSON,
10+ validateEveryToJSONWithPatternChecker,
11+) where
12+
13+import Servant.Swagger.Internal.Test
servant-swagger/src/Servant/Swagger/TypeLevel.hsnew file+15
@@ -0,0 +1,15 @@
1+-- |
2+-- Module: Servant.Swagger.TypeLevel
3+-- License: BSD3
4+-- Maintainer: Nickolay Kudasov <nickolay@getshoptv.com>
5+-- Stability: experimental
6+--
7+-- Useful type families for servant APIs.
8+module Servant.Swagger.TypeLevel (
9+ IsSubAPI,
10+ EndpointsList,
11+ BodyTypes,
12+) where
13+
14+import Servant.Swagger.Internal.TypeLevel
15+
servant-swagger/test/Servant/SwaggerSpec.hsnew file+489
@@ -0,0 +1,489 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE DeriveDataTypeable #-}
4+{-# LANGUAGE DeriveGeneric #-}
5+{-# LANGUAGE OverloadedStrings #-}
6+{-# LANGUAGE QuasiQuotes #-}
7+{-# LANGUAGE TypeFamilies #-}
8+{-# LANGUAGE TypeOperators #-}
9+{-# LANGUAGE PackageImports #-}
10+module Servant.SwaggerSpec where
11+
12+import Control.Lens
13+import Data.Aeson (ToJSON(toJSON), Value, genericToJSON)
14+import Data.Aeson.QQ.Simple
15+import qualified Data.Aeson.Types as JSON
16+import Data.Char (toLower)
17+import Data.Int (Int64)
18+import Data.Proxy
19+import Data.Swagger
20+import Data.Text (Text)
21+import Data.Time
22+import GHC.Generics
23+import Servant.API
24+import Servant.Swagger
25+import Servant.Test.ComprehensiveAPI (comprehensiveAPI)
26+import Test.Hspec hiding (example)
27+
28+#if !MIN_VERSION_swagger2(2,4,0)
29+import Data.Aeson.Lens (key, _Array)
30+import qualified Data.Vector as V
31+#endif
32+
33+checkAPI :: HasSwagger api => Proxy api -> Value -> IO ()
34+checkAPI proxy = checkSwagger (toSwagger proxy)
35+
36+checkSwagger :: Swagger -> Value -> IO ()
37+checkSwagger swag js = toJSON swag `shouldBe` js
38+
39+spec :: Spec
40+spec = describe "HasSwagger" $ do
41+ it "Todo API" $ checkAPI (Proxy :: Proxy TodoAPI) todoAPI
42+ it "Hackage API (with tags)" $ checkSwagger hackageSwaggerWithTags hackageAPI
43+ it "GetPost API (test subOperations)" $ checkSwagger getPostSwagger getPostAPI
44+ it "UVerb API" $ checkSwagger uverbSwagger uverbAPI
45+ it "Comprehensive API" $ do
46+ let _x = toSwagger comprehensiveAPI
47+ True `shouldBe` True -- type-level test
48+
49+main :: IO ()
50+main = hspec spec
51+
52+-- =======================================================================
53+-- Todo API
54+-- =======================================================================
55+
56+data Todo = Todo
57+ { created :: UTCTime
58+ , title :: String
59+ , summary :: Maybe String
60+ } deriving (Generic)
61+
62+instance ToJSON Todo
63+instance ToSchema Todo
64+
65+newtype TodoId = TodoId String deriving (Generic)
66+instance ToParamSchema TodoId
67+
68+type TodoAPI = "todo" :> Capture "id" TodoId :> Get '[JSON] Todo
69+
70+todoAPI :: Value
71+todoAPI = [aesonQQ|
72+{
73+ "swagger":"2.0",
74+ "info":
75+ {
76+ "title": "",
77+ "version": ""
78+ },
79+ "definitions":
80+ {
81+ "Todo":
82+ {
83+ "type": "object",
84+ "required": [ "created", "title" ],
85+ "properties":
86+ {
87+ "created": { "$ref": "#/definitions/UTCTime" },
88+ "title": { "type": "string" },
89+ "summary": { "type": "string" }
90+ }
91+ },
92+ "UTCTime":
93+ {
94+ "type": "string",
95+ "format": "yyyy-mm-ddThh:MM:ssZ",
96+ "example": "2016-07-22T00:00:00Z"
97+ }
98+ },
99+ "paths":
100+ {
101+ "/todo/{id}":
102+ {
103+ "get":
104+ {
105+ "responses":
106+ {
107+ "200":
108+ {
109+ "schema": { "$ref":"#/definitions/Todo" },
110+ "description": ""
111+ },
112+ "400": { "description": "Invalid `id`" }
113+ },
114+ "produces": [ "application/json;charset=utf-8" ],
115+ "parameters":
116+ [
117+ {
118+ "required": true,
119+ "in": "path",
120+ "name": "id",
121+ "type": "string"
122+ }
123+ ]
124+ }
125+ }
126+ }
127+}
128+|]
129+
130+-- =======================================================================
131+-- Hackage API
132+-- =======================================================================
133+
134+type HackageAPI
135+ = HackageUserAPI
136+ :<|> HackagePackagesAPI
137+
138+type HackageUserAPI =
139+ "users" :> Get '[JSON] [UserSummary]
140+ :<|> "user" :> Capture "username" Username :> Get '[JSON] UserDetailed
141+
142+type HackagePackagesAPI
143+ = "packages" :> Get '[JSON] [Package]
144+
145+type Username = Text
146+
147+data UserSummary = UserSummary
148+ { summaryUsername :: Username
149+ , summaryUserid :: Int64 -- Word64 would make sense too
150+ } deriving (Eq, Show, Generic)
151+
152+lowerCutPrefix :: String -> String -> String
153+lowerCutPrefix s = map toLower . drop (length s)
154+
155+instance ToJSON UserSummary where
156+ toJSON = genericToJSON JSON.defaultOptions { JSON.fieldLabelModifier = lowerCutPrefix "summary" }
157+
158+instance ToSchema UserSummary where
159+ declareNamedSchema proxy = genericDeclareNamedSchema defaultSchemaOptions { fieldLabelModifier = lowerCutPrefix "summary" } proxy
160+ & mapped.schema.example ?~ toJSON UserSummary
161+ { summaryUsername = "JohnDoe"
162+ , summaryUserid = 123 }
163+
164+type Group = Text
165+
166+data UserDetailed = UserDetailed
167+ { username :: Username
168+ , userid :: Int64
169+ , groups :: [Group]
170+ } deriving (Eq, Show, Generic)
171+instance ToSchema UserDetailed
172+
173+newtype Package = Package { packageName :: Text }
174+ deriving (Eq, Show, Generic)
175+instance ToSchema Package
176+
177+hackageSwaggerWithTags :: Swagger
178+hackageSwaggerWithTags = toSwagger (Proxy :: Proxy HackageAPI)
179+ & host ?~ Host "hackage.haskell.org" Nothing
180+ & applyTagsFor usersOps ["users" & description ?~ "Operations about user"]
181+ & applyTagsFor packagesOps ["packages" & description ?~ "Query packages"]
182+ where
183+ usersOps, packagesOps :: Traversal' Swagger Operation
184+ usersOps = subOperations (Proxy :: Proxy HackageUserAPI) (Proxy :: Proxy HackageAPI)
185+ packagesOps = subOperations (Proxy :: Proxy HackagePackagesAPI) (Proxy :: Proxy HackageAPI)
186+
187+hackageAPI :: Value
188+hackageAPI = modifyValue [aesonQQ|
189+{
190+ "swagger":"2.0",
191+ "host":"hackage.haskell.org",
192+ "info":{
193+ "version":"",
194+ "title":""
195+ },
196+ "definitions":{
197+ "UserDetailed":{
198+ "required":[
199+ "username",
200+ "userid",
201+ "groups"
202+ ],
203+ "type":"object",
204+ "properties":{
205+ "groups":{
206+ "items":{
207+ "type":"string"
208+ },
209+ "type":"array"
210+ },
211+ "username":{
212+ "type":"string"
213+ },
214+ "userid":{
215+ "maximum":9223372036854775807,
216+ "minimum":-9223372036854775808,
217+ "type":"integer",
218+ "format":"int64"
219+ }
220+ }
221+ },
222+ "Package":{
223+ "required":[
224+ "packageName"
225+ ],
226+ "type":"object",
227+ "properties":{
228+ "packageName":{
229+ "type":"string"
230+ }
231+ }
232+ },
233+ "UserSummary":{
234+ "required":[
235+ "username",
236+ "userid"
237+ ],
238+ "type":"object",
239+ "properties":{
240+ "username":{
241+ "type":"string"
242+ },
243+ "userid":{
244+ "maximum":9223372036854775807,
245+ "minimum":-9223372036854775808,
246+ "type":"integer",
247+ "format":"int64"
248+ }
249+ },
250+ "example":{
251+ "username": "JohnDoe",
252+ "userid": 123
253+ }
254+ }
255+ },
256+ "paths":{
257+ "/users":{
258+ "get":{
259+ "responses":{
260+ "200":{
261+ "schema":{
262+ "items":{
263+ "$ref":"#/definitions/UserSummary"
264+ },
265+ "type":"array"
266+ },
267+ "description":""
268+ }
269+ },
270+ "produces":[
271+ "application/json;charset=utf-8"
272+ ],
273+ "tags":[
274+ "users"
275+ ]
276+ }
277+ },
278+ "/packages":{
279+ "get":{
280+ "responses":{
281+ "200":{
282+ "schema":{
283+ "items":{
284+ "$ref":"#/definitions/Package"
285+ },
286+ "type":"array"
287+ },
288+ "description":""
289+ }
290+ },
291+ "produces":[
292+ "application/json;charset=utf-8"
293+ ],
294+ "tags":[
295+ "packages"
296+ ]
297+ }
298+ },
299+ "/user/{username}":{
300+ "get":{
301+ "responses":{
302+ "400":{
303+ "description":"Invalid `username`"
304+ },
305+ "200":{
306+ "schema":{
307+ "$ref":"#/definitions/UserDetailed"
308+ },
309+ "description":""
310+ }
311+ },
312+ "produces":[
313+ "application/json;charset=utf-8"
314+ ],
315+ "parameters":[
316+ {
317+ "required":true,
318+ "in":"path",
319+ "name":"username",
320+ "type":"string"
321+ }
322+ ],
323+ "tags":[
324+ "users"
325+ ]
326+ }
327+ }
328+ },
329+ "tags":[
330+ {
331+ "name":"users",
332+ "description":"Operations about user"
333+ },
334+ {
335+ "name":"packages",
336+ "description":"Query packages"
337+ }
338+ ]
339+}
340+|]
341+ where
342+ modifyValue :: Value -> Value
343+#if MIN_VERSION_swagger2(2,4,0)
344+ modifyValue = id
345+#else
346+ -- swagger2-2.4 preserves order of tags
347+ -- swagger2-2.3 used Set, so they are ordered
348+ -- packages comes before users.
349+ -- We simply reverse, not properly sort here for simplicity: 2 elements.
350+ modifyValue = over (key "tags" . _Array) V.reverse
351+#endif
352+
353+
354+-- =======================================================================
355+-- Get/Post API (test for subOperations)
356+-- =======================================================================
357+
358+type GetPostAPI = Get '[JSON] String :<|> Post '[JSON] String
359+
360+getPostSwagger :: Swagger
361+getPostSwagger = toSwagger (Proxy :: Proxy GetPostAPI)
362+ & applyTagsFor getOps ["get" & description ?~ "GET operations"]
363+ where
364+ getOps :: Traversal' Swagger Operation
365+ getOps = subOperations (Proxy :: Proxy (Get '[JSON] String)) (Proxy :: Proxy GetPostAPI)
366+
367+getPostAPI :: Value
368+getPostAPI = [aesonQQ|
369+{
370+ "swagger":"2.0",
371+ "info":{
372+ "version":"",
373+ "title":""
374+ },
375+ "paths":{
376+ "/":{
377+ "post":{
378+ "responses":{
379+ "200":{
380+ "schema":{
381+ "type":"string"
382+ },
383+ "description":""
384+ }
385+ },
386+ "produces":[ "application/json;charset=utf-8" ]
387+ },
388+ "get":{
389+ "responses":{
390+ "200":{
391+ "schema":{
392+ "type":"string"
393+ },
394+ "description":""
395+ }
396+ },
397+ "produces":[ "application/json;charset=utf-8" ],
398+ "tags":[ "get" ]
399+ }
400+ }
401+ },
402+ "tags":[
403+ {
404+ "name":"get",
405+ "description":"GET operations"
406+ }
407+ ]
408+}
409+|]
410+
411+-- =======================================================================
412+-- UVerb API
413+-- =======================================================================
414+
415+data Lunch = Lunch {name :: String}
416+ deriving (Eq, Show, Generic)
417+
418+instance ToSchema Lunch
419+
420+instance HasStatus Lunch where
421+ type StatusOf Lunch = 200
422+
423+data NoLunch = NoLunch
424+ deriving (Eq, Show, Generic)
425+
426+instance ToSchema NoLunch
427+
428+instance HasStatus NoLunch where
429+ type StatusOf NoLunch = 404
430+
431+type UVerbAPI2 =
432+ "lunch" :> UVerb 'GET '[JSON] '[Lunch, NoLunch]
433+
434+uverbSwagger :: Swagger
435+uverbSwagger = toSwagger (Proxy :: Proxy UVerbAPI2)
436+
437+uverbAPI :: Value
438+uverbAPI =
439+ [aesonQQ|
440+ {
441+ "swagger": "2.0",
442+ "info": {
443+ "version": "",
444+ "title": ""
445+ },
446+ "definitions": {
447+ "Lunch": {
448+ "required": [
449+ "name"
450+ ],
451+ "type": "object",
452+ "properties": {
453+ "name": {
454+ "type": "string"
455+ }
456+ }
457+ },
458+ "NoLunch": {
459+ "type": "string",
460+ "enum": [
461+ "NoLunch"
462+ ]
463+ }
464+ },
465+ "paths": {
466+ "/lunch": {
467+ "get": {
468+ "responses": {
469+ "404": {
470+ "schema": {
471+ "$ref": "#/definitions/NoLunch"
472+ },
473+ "description": ""
474+ },
475+ "200": {
476+ "schema": {
477+ "$ref": "#/definitions/Lunch"
478+ },
479+ "description": ""
480+ }
481+ },
482+ "produces": [
483+ "application/json;charset=utf-8"
484+ ]
485+ }
486+ }
487+ }
488+}
489+|]
servant-swagger/test/Spec.hsnew file+1
@@ -0,0 +1 @@
1+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
servant-swagger/test/doctests.hsnew file+12
@@ -0,0 +1,12 @@
1+module Main where
2+
3+import Build_doctests (flags, pkgs, module_sources)
4+import Data.Foldable (traverse_)
5+import Test.DocTest
6+
7+main :: IO ()
8+main = do
9+ traverse_ putStrLn args
10+ doctest args
11+ where
12+ args = flags ++ pkgs ++ module_sources
servant/CHANGELOG.md+116
@@ -1,5 +1,121 @@
11[The latest version of this document is on GitHub.](https://github.com/haskell-servant/servant/blob/master/servant/CHANGELOG.md)
22
3+Package versions follow the [Package Versioning Policy](https://pvp.haskell.org/): in A.B.C, bumps to either A or B represent major versions.
4+
5+0.19
6+----
7+
8+### Significant changes
9+
10+- Drop support for GHC < 8.6.
11+- Support GHC 9.0 (GHC 9.2 should work as well, but isn't fully tested yet).
12+- Support Aeson 2 ([#1475](https://github.com/haskell-servant/servant/pull/1475)),
13+ which fixes a [DOS vulnerability](https://github.com/haskell/aeson/issues/864)
14+ related to hash collisions.
15+- Add `NamedRoutes` combinator, making support for records first-class in Servant
16+ ([#1388](https://github.com/haskell-servant/servant/pull/1388)).
17+
18+ Users can now directly mark part as an API as defined by a record, instead of
19+ using `(:<|>)` to combine routes. Concretely, the anonymous:
20+
21+ ```haskell
22+ type API =
23+ "version" :> Get '[JSON] String :<|>
24+ "products" :> Get '[JSON] [Product]
25+ ```
26+
27+ can be replaced with the explicitly-named:
28+
29+ ```haskell
30+ type API = NamedRoutes NamedAPI
31+ data NamedAPI mode = NamedAPI
32+ { version :: mode :- "version" :> Get '[JSON] String
33+ , products :: mode :- "products" :> Get '[JSON] [Product]
34+ }
35+ ```
36+
37+ `NamedRoutes` builds upon `servant-generic`, but improves usability by freeing
38+ users from the need to perform `toServant` / `fromServant` conversions
39+ manually. Serving `NamedRoutes NamedAPI` is now done directly by providing a
40+ record of handlers, and servant generates clients directly as records as well.
41+ In particular, it makes it much more practical to work with nested hierarchies
42+ of named routes.
43+
44+ Two convenience functions, `(//)` and `(/:)`, have been added to make the
45+ usage of named route hierarchies more pleasant:
46+
47+ ```haskell
48+ rootClient :: RootApi (AsClientT ClientM)
49+ rootClient = client (Proxy @API)
50+
51+ helloClient :: String -> ClientM String
52+ helloClient name = rootClient // hello /: name
53+
54+ endpointClient :: ClientM Person
55+ endpointClient = rootClient // subApi /: "foobar123" // endpoint
56+
57+ type Api = NamedRoutes RootApi
58+
59+ data RootApi mode = RootApi
60+ { subApi :: mode :- Capture "token" String :> NamedRoutes SubApi
61+ , hello :: mode :- Capture "name" String :> Get '[JSON] String
62+ , …
63+ } deriving Generic
64+
65+ data SubApi mode = SubApi
66+ { endpoint :: mode :- Get '[JSON] Person
67+ , …
68+ } deriving Generic
69+ ```
70+
71+- Add custom type errors for partially applied combinators
72+ ([#1289](https://github.com/haskell-servant/servant/pull/1289),
73+ [#1486](https://github.com/haskell-servant/servant/pull/1486)).
74+
75+ For example, forgetting to document the expected type for a query parameter,
76+ as in:
77+
78+ ``` haskell
79+ type API = QueryParam "param" :> Get '[JSON] NoContent
80+ ```
81+
82+ will raise to the following error when trying to serve the API:
83+
84+ ```
85+ • There is no instance for HasServer (QueryParam'
86+ '[Optional, Strict] "param" :> ...)
87+ QueryParam' '[Optional, Strict] "1" expects 1 more arguments
88+ ```
89+
90+ As a consequence of this change, unsaturated types are now forbidden before `(:>)`.
91+
92+- Add a `HeadNoContent` verb ([#1502](https://github.com/haskell-servant/servant/pull/1502)).
93+
94+- *servant-client* / *servant-client-core* / *servant-http-streams*:
95+ Fix erroneous behavior, where only 2XX status codes would be considered
96+ successful, irrelevant of the status parameter specified by the verb
97+ combinator. ([#1469](https://github.com/haskell-servant/servant/pull/1469))
98+
99+- *servant-client* / *servant-client-core*: Fix `Show` instance for
100+ `Servant.Client.Core.Request`.
101+
102+
103+- *servant-client* / *servant-client-core*: Allow passing arbitrary binary data
104+ in Query parameters.
105+ ([#1432](https://github.com/haskell-servant/servant/pull/1432)).
106+
107+- *servant-docs*: Generate sample cURL requests
108+ ([#1401](https://github.com/haskell-servant/servant/pull/1401/files)).
109+
110+ Breaking change: requires sample header values to be supplied with `headers`.
111+
112+### Other changes
113+
114+- Various bit rotten cookbooks have been updated and re-introduced on
115+ [docs.servant.dev](https://docs.servant.dev).
116+
117+- Various version bumps.
118+
31190.18.3
4120------
5121
servant/servant.cabal+12−9
@@ -1,6 +1,6 @@
1−cabal-version: >=1.10
1+cabal-version: 2.2
22name: servant
3−version: 0.18.3
3+version: 0.19
44
55synopsis: A family of combinators for defining webservices APIs
66category: Servant, Web
@@ -46,6 +46,7 @@ library
4646 Servant.API.HttpVersion
4747 Servant.API.IsSecure
4848 Servant.API.Modifiers
49+ Servant.API.NamedRoutes
4950 Servant.API.QueryParam
5051 Servant.API.Raw
5152 Servant.API.RemoteHost
@@ -54,6 +55,7 @@ library
5455 Servant.API.Status
5556 Servant.API.Stream
5657 Servant.API.Sub
58+ Servant.API.TypeErrors
5759 Servant.API.TypeLevel
5860 Servant.API.UVerb
5961 Servant.API.UVerb.Union
@@ -78,12 +80,13 @@ library
7880 --
7981 -- note: mtl lower bound is so low because of GHC-7.8
8082 build-depends:
81− base >= 4.9 && < 4.16
83+ base >= 4.9 && < 4.17
8284 , bytestring >= 0.10.8.1 && < 0.12
85+ , constraints >= 0.2
8386 , mtl >= 2.2.2 && < 2.3
8487 , sop-core >= 0.4.0.0 && < 0.6
8588 , transformers >= 0.5.2.0 && < 0.6
86− , text >= 1.2.3.0 && < 1.3
89+ , text >= 1.2.3.0 && < 2.1
8790
8891
8992 -- We depend (heavily) on the API of these packages:
@@ -95,15 +98,15 @@ library
9598 -- Other dependencies: Lower bound around what is in the latest Stackage LTS.
9699 -- Here can be exceptions if we really need features from the newer versions.
97100 build-depends:
98− base-compat >= 0.10.5 && < 0.12
99− , aeson >= 1.4.1.0 && < 1.6
101+ base-compat >= 0.10.5 && < 0.13
102+ , aeson >= 1.4.1.0 && < 3
100103 , attoparsec >= 0.13.2.2 && < 0.15
101104 , bifunctors >= 5.5.3 && < 5.6
102105 , case-insensitive >= 1.2.0.11 && < 1.3
103106 , deepseq >= 1.4.2.0 && < 1.5
104107 , http-media >= 0.7.1.3 && < 0.9
105108 , http-types >= 0.12.2 && < 0.13
106− , mmorph >= 1.1.2 && < 1.2
109+ , mmorph >= 1.1.2 && < 1.3
107110 , network-uri >= 2.6.1.0 && < 2.7
108111 , QuickCheck >= 2.12.6.1 && < 2.15
109112 , string-conversions >= 0.4.0.1 && < 0.5
@@ -163,9 +166,9 @@ test-suite spec
163166
164167 -- Additional dependencies
165168 build-depends:
166− hspec >= 2.6.0 && < 2.9
169+ hspec >= 2.6.0 && < 2.10
167170 , QuickCheck >= 2.12.6.1 && < 2.15
168171 , quickcheck-instances >= 0.3.19 && < 0.4
169172
170173 build-tool-depends:
171− hspec-discover:hspec-discover >= 2.6.0 && < 2.9
174+ hspec-discover:hspec-discover >= 2.6.0 && < 2.10
servant/src/Servant/API.hs+9
@@ -36,6 +36,10 @@ module Servant.API (
3636 module Servant.API.Verbs,
3737 module Servant.API.UVerb,
3838
39+ -- * Sub-APIs defined as records of routes
40+ module Servant.API.NamedRoutes,
41+ module Servant.API.Generic,
42+
3943 -- * Streaming endpoints, distinguished by HTTP method
4044 module Servant.API.Stream,
4145
@@ -99,6 +103,9 @@ import Servant.API.Fragment
99103 (Fragment)
100104import Servant.API.Header
101105 (Header, Header')
106+import Servant.API.Generic
107+ (GenericMode ((:-)), AsApi, ToServant, ToServantApi, GServantProduct,
108+ GenericServant, fromServant, toServant, genericApi)
102109import Servant.API.HttpVersion
103110 (HttpVersion (..))
104111import Servant.API.IsSecure
@@ -130,6 +137,8 @@ import Servant.API.UVerb
130137 Unique, WithStatus (..), inject, statusOf)
131138import Servant.API.Vault
132139 (Vault)
140+import Servant.API.NamedRoutes
141+ (NamedRoutes)
133142import Servant.API.Verbs
134143 (Delete, DeleteAccepted, DeleteNoContent,
135144 DeleteNonAuthoritative, Get, GetAccepted, GetNoContent,
servant/src/Servant/API/ContentTypes.hs+1−1
@@ -295,7 +295,7 @@ instance {-# OVERLAPPABLE #-}
295295-- then this would be taken care of. However there is no more specific instance
296296-- between that and 'MimeRender JSON a', so we do this instead
297297instance {-# OVERLAPPING #-} ( Accept ctyp ) => AllMimeRender '[ctyp] NoContent where
298− allMimeRender _ _ = map (, "") $ NE.toList $ contentTypes pctyp
298+ allMimeRender _ NoContent = map (, "") $ NE.toList $ contentTypes pctyp
299299 where
300300 pctyp = Proxy :: Proxy ctyp
301301
servant/src/Servant/API/NamedRoutes.hsnew file+10
@@ -0,0 +1,10 @@
1+{-# LANGUAGE KindSignatures #-}
2+{-# OPTIONS_HADDOCK not-home #-}
3+
4+module Servant.API.NamedRoutes (
5+ -- * NamedRoutes combinator
6+ NamedRoutes
7+ ) where
8+
9+-- | Combinator for embedding a record of named routes into a Servant API type.
10+data NamedRoutes (api :: * -> *)
servant/src/Servant/API/ResponseHeaders.hs+6−11
@@ -3,10 +3,8 @@
33{-# LANGUAGE DeriveFunctor #-}
44{-# LANGUAGE FlexibleContexts #-}
55{-# LANGUAGE FlexibleInstances #-}
6−{-# LANGUAGE FunctionalDependencies #-}
76{-# LANGUAGE GADTs #-}
8−{-# LANGUAGE KindSignatures #-}
9−{-# LANGUAGE MultiParamTypeClasses #-}
7+{-# LANGUAGE FunctionalDependencies #-}
108{-# LANGUAGE PolyKinds #-}
119{-# LANGUAGE ScopedTypeVariables #-}
1210{-# LANGUAGE TypeFamilies #-}
@@ -51,9 +49,6 @@ import Web.HttpApiData
5149
5250import Prelude ()
5351import Prelude.Compat
54−import Servant.API.ContentTypes
55− (JSON, PlainText, FormUrlEncoded, OctetStream,
56− MimeRender(..))
5752import Servant.API.Header
5853 (Header)
5954
@@ -117,7 +112,7 @@ instance {-# OVERLAPPABLE #-} ( FromHttpApiData v, BuildHeadersTo xs, KnownSymbo
117112 `HCons` buildHeadersTo headers
118113 Right h -> Header h `HCons` buildHeadersTo headers
119114
120−-- * Getting
115+-- * Getting headers
121116
122117class GetHeaders ls where
123118 getHeaders :: ls -> [HTTP.Header]
@@ -158,20 +153,20 @@ instance (KnownSymbol h, GetHeadersFromHList rest, ToHttpApiData v)
158153 where
159154 getHeaders' hs = getHeadersFromHList $ getHeadersHList hs
160155
161−-- * Adding
156+-- * Adding headers
162157
163158-- We need all these fundeps to save type inference
164159class AddHeader h v orig new
165160 | h v orig -> new, new -> h, new -> v, new -> orig where
166161 addOptionalHeader :: ResponseHeader h v -> orig -> new -- ^ N.B.: The same header can't be added multiple times
167162
168−
163+-- In this instance, we add a Header on top of something that is already decorated with some headers
169164instance {-# OVERLAPPING #-} ( KnownSymbol h, ToHttpApiData v )
170165 => AddHeader h v (Headers (fst ': rest) a) (Headers (Header h v ': fst ': rest) a) where
171166 addOptionalHeader hdr (Headers resp heads) = Headers resp (HCons hdr heads)
172167
173−instance {-# OVERLAPPABLE #-} ( KnownSymbol h, ToHttpApiData v
174− , new ~ (Headers '[Header h v] a) )
168+-- In this instance, 'a' parameter is decorated with a Header.
169+instance {-# OVERLAPPABLE #-} ( KnownSymbol h, ToHttpApiData v , new ~ Headers '[Header h v] a)
175170 => AddHeader h v a new where
176171 addOptionalHeader hdr resp = Headers resp (HCons hdr HNil)
177172
servant/src/Servant/API/Status.hs+6−1
@@ -1,10 +1,15 @@
11{-# LANGUAGE DataKinds #-}
22-- Flexible instances is necessary on GHC 8.4 and earlier
33{-# LANGUAGE FlexibleInstances #-}
4+{-# LANGUAGE ScopedTypeVariables #-}
45module Servant.API.Status where
56
7+import GHC.TypeLits (KnownNat, natVal)
68import Network.HTTP.Types.Status
7−import GHC.TypeLits
9+
10+-- | Retrieve a known or unknown Status from a KnownNat
11+statusFromNat :: forall a proxy. KnownNat a => proxy a -> Status
12+statusFromNat = toEnum . fromInteger . natVal
813
914-- | Witness that a type-level natural number corresponds to a HTTP status code
1015class KnownNat n => KnownStatus n where
servant/src/Servant/API/TypeErrors.hsnew file+40
@@ -0,0 +1,40 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE PolyKinds #-}
3+{-# LANGUAGE TypeFamilies #-}
4+{-# LANGUAGE TypeOperators #-}
5+{-# LANGUAGE UndecidableInstances #-}
6+
7+-- | This module defines the error messages used in type-level errors.
8+-- Type-level errors can signal non-existing instances, for instance when
9+-- a combinator is not applied to the correct number of arguments.
10+
11+module Servant.API.TypeErrors (
12+ PartialApplication,
13+ NoInstanceFor,
14+ NoInstanceForSub,
15+) where
16+
17+import Data.Kind
18+import GHC.TypeLits
19+
20+-- | No instance exists for @tycls (expr :> ...)@ because
21+-- @expr@ is not recognised.
22+type NoInstanceForSub (tycls :: k) (expr :: k') =
23+ Text "There is no instance for " :<>: ShowType tycls
24+ :<>: Text " (" :<>: ShowType expr :<>: Text " :> ...)"
25+
26+-- | No instance exists for @expr@.
27+type NoInstanceFor (expr :: k) =
28+ Text "There is no instance for " :<>: ShowType expr
29+
30+-- | No instance exists for @tycls (expr :> ...)@ because @expr@ is not fully saturated.
31+type PartialApplication (tycls :: k) (expr :: k') =
32+ NoInstanceForSub tycls expr
33+ :$$: ShowType expr :<>: Text " expects " :<>: ShowType (Arity expr) :<>: Text " more arguments"
34+
35+-- The arity of a combinator, i.e. the number of required arguments.
36+type Arity (ty :: k) = Arity' k
37+
38+type family Arity' (ty :: k) :: Nat where
39+ Arity' (_ -> ty) = 1 + Arity' ty
40+ Arity' _ = 0
servant/src/Servant/API/Verbs.hs+2−1
@@ -128,7 +128,8 @@ type DeleteNoContent = NoContentVerb 'DELETE
128128type PatchNoContent = NoContentVerb 'PATCH
129129-- | 'PUT' with 204 status code.
130130type PutNoContent = NoContentVerb 'PUT
131−
131+-- | 'HEAD' with 204 status code.
132+type HeadNoContent = NoContentVerb 'HEAD
132133
133134-- ** 205 Reset Content
134135--
servant/src/Servant/Links.hs+49−1
@@ -1,13 +1,18 @@
1+{-# LANGUAGE AllowAmbiguousTypes #-}
12{-# LANGUAGE ConstraintKinds #-}
23{-# LANGUAGE DataKinds #-}
34{-# LANGUAGE FlexibleContexts #-}
45{-# LANGUAGE FlexibleInstances #-}
56{-# LANGUAGE FunctionalDependencies #-}
7+{-# LANGUAGE InstanceSigs #-}
68{-# LANGUAGE PolyKinds #-}
9+{-# LANGUAGE QuantifiedConstraints #-}
710{-# LANGUAGE ScopedTypeVariables #-}
11+{-# LANGUAGE TypeApplications #-}
812{-# LANGUAGE TypeFamilies #-}
913{-# LANGUAGE TypeOperators #-}
1014{-# LANGUAGE UndecidableInstances #-}
15+
1116{-# OPTIONS_HADDOCK not-home #-}
1217
1318-- | Type safe generation of internal links.
@@ -125,6 +130,7 @@ module Servant.Links (
125130) where
126131
127132import Data.List
133+import Data.Constraint
128134import Data.Proxy
129135 (Proxy (..))
130136import Data.Singletons.Bool
@@ -134,7 +140,7 @@ import qualified Data.Text.Encoding as TE
134140import Data.Type.Bool
135141 (If)
136142import GHC.TypeLits
137− (KnownSymbol, symbolVal)
143+ (KnownSymbol, TypeError, symbolVal)
138144import Network.URI
139145 (URI (..), escapeURIString, isUnreserved)
140146import Prelude ()
@@ -163,6 +169,8 @@ import Servant.API.IsSecure
163169 (IsSecure)
164170import Servant.API.Modifiers
165171 (FoldRequired)
172+import Servant.API.NamedRoutes
173+ (NamedRoutes)
166174import Servant.API.QueryParam
167175 (QueryFlag, QueryParam', QueryParams)
168176import Servant.API.Raw
@@ -175,6 +183,7 @@ import Servant.API.Stream
175183 (Stream, StreamBody')
176184import Servant.API.Sub
177185 (type (:>))
186+import Servant.API.TypeErrors
178187import Servant.API.TypeLevel
179188import Servant.API.UVerb
180189import Servant.API.Vault
@@ -579,6 +588,34 @@ instance HasLink (Stream m status fr ct a) where
579588instance HasLink (UVerb m ct a) where
580589 type MkLink (UVerb m ct a) r = r
581590 toLink toA _ = toA
591+-- Instance for NamedRoutes combinator
592+
593+type GLinkConstraints routes a =
594+ ( MkLink (ToServant routes AsApi) a ~ ToServant routes (AsLink a)
595+ , GenericServant routes (AsLink a)
596+ )
597+
598+class GLink (routes :: * -> *) (a :: *) where
599+ gLinkProof :: Dict (GLinkConstraints routes a)
600+
601+instance GLinkConstraints routes a => GLink routes a where
602+ gLinkProof = Dict
603+
604+instance
605+ ( HasLink (ToServantApi routes)
606+ , forall a. GLink routes a
607+ ) => HasLink (NamedRoutes routes) where
608+
609+ type MkLink (NamedRoutes routes) a = routes (AsLink a)
610+
611+ toLink
612+ :: forall a. (Link -> a)
613+ -> Proxy (NamedRoutes routes)
614+ -> Link
615+ -> routes (AsLink a)
616+
617+ toLink toA _ l = case gLinkProof @routes @a of
618+ Dict -> fromServant $ toLink toA (Proxy @(ToServantApi routes)) l
582619
583620-- AuthProtext instances
584621instance HasLink sub => HasLink (AuthProtect tag :> sub) where
@@ -608,3 +645,14 @@ simpleToLink _ toA _ = toLink toA (Proxy :: Proxy sub)
608645-- $setup
609646-- >>> import Servant.API
610647-- >>> import Data.Text (Text)
648+
649+-- Erroring instance for 'HasLink' when a combinator is not fully applied
650+instance TypeError (PartialApplication HasLink arr) => HasLink ((arr :: a -> b) :> sub)
651+ where
652+ type MkLink (arr :> sub) _ = TypeError (PartialApplication (HasLink :: * -> Constraint) arr)
653+ toLink = error "unreachable"
654+
655+-- Erroring instances for 'HasLink' for unknown API combinators
656+instance {-# OVERLAPPABLE #-} TypeError (NoInstanceForSub HasLink ty) => HasLink (ty :> sub)
657+
658+instance {-# OVERLAPPABLE #-} TypeError (NoInstanceFor (HasLink api)) => HasLink api
servant/src/Servant/Types/SourceT.hs+1−1
@@ -319,7 +319,7 @@ fromActionStep stop action = loop where
319319-- | Read file.
320320--
321321-- >>> foreach fail BS.putStr (readFile "servant.cabal")
322−-- cabal-version: >=1.10
322+-- cabal-version: 2.2
323323-- name: servant
324324-- ...
325325--
servant/test/Servant/API/ContentTypesSpec.hs+11
@@ -33,6 +33,8 @@ import Data.String.Conversions
3333import qualified Data.Text as TextS
3434import qualified Data.Text.Encoding as TextSE
3535import qualified Data.Text.Lazy as TextL
36+import Control.Exception
37+ (evaluate)
3638import GHC.Generics
3739import Test.Hspec
3840import Test.QuickCheck
@@ -78,6 +80,15 @@ spec = describe "Servant.API.ContentTypes" $ do
7880 it "has mimeUnrender reverse mimeRender for valid top-level json " $ do
7981 property $ \x -> mimeUnrender p (mimeRender p x) == Right (x::SomeData)
8082
83+ describe "The NoContent Content-Type type" $ do
84+ let p = Proxy :: Proxy '[JSON]
85+
86+ it "does not render any content" $
87+ allMimeRender p NoContent `shouldSatisfy` (all (BSL8.null . snd))
88+
89+ it "evaluates the NoContent value" $
90+ evaluate (allMimeRender p (undefined :: NoContent)) `shouldThrow` anyErrorCall
91+
8192 describe "The PlainText Content-Type type" $ do
8293 let p = Proxy :: Proxy PlainText
8394
stack.yaml+1
@@ -11,6 +11,7 @@ packages:
1111- servant-conduit
1212- servant-machines/
1313- servant-pipes/
14+- servant-swagger/
1415
1516# allow-newer: true # ignores all bounds, that's a sledgehammer
1617# - doc/tutorial/