git.iko.soy/typeable/servant
on 0be69cb help sign in
27 files changed+790−643
since splitsnapshots
unifiedsplit
.travis.yml+30−11 cabal.make-travis-yml+1−1 cabal.project+2−8 doc/index.rst+11−9 doc/introduction.rst → doc/principles.rst+2−2 doc/tutorial/Javascript.lhs+1−2 doc/tutorial/index.rst+21−2 servant-client-core/CHANGELOG.md+42−2 servant-client-core/servant-client-core.cabal+12−13 servant-client-ghcjs/servant-client-ghcjs.cabal+1−1 servant-client/CHANGELOG.md+37−2 servant-client/servant-client.cabal+17−23 servant-client/test/Servant/StreamSpec.hs+2 servant-docs/servant-docs.cabal+1−1 servant-foreign/servant-foreign.cabal+1 servant-server/CHANGELOG.md+46 servant-server/servant-server.cabal+23−31 servant-server/src/Servant.hs+2−2 servant-server/src/Servant/Server/Internal/Context.hs+1−1 servant/CHANGELOG.md+14−5 servant/servant.cabal+24−21 servant/src/Servant/API.hs+3−3 servant/src/Servant/API/Verbs.hs+1−1 servant/src/Servant/Links.hs+487 servant/src/Servant/Utils/Links.hs+4−485 servant/test/Servant/Utils/LinksSpec.hs → servant/test/Servant/LinksSpec.hs+3−3 stack.yaml+1−14
.travis.yml+30−11
@@ -13,7 +13,7 @@ git:
1313branches:
1414 only:
1515 - master
16− - release-0.12
16+ - release-0.14
1717
1818cache:
1919 directories:
@@ -65,17 +65,21 @@ install:
6565 - BENCH=${BENCH---enable-benchmarks}
6666 - TEST=${TEST---enable-tests}
6767 - HADDOCK=${HADDOCK-true}
68− - INSTALLED=${INSTALLED-true}
68+ - UNCONSTRAINED=${UNCONSTRAINED-true}
69+ - NOINSTALLEDCONSTRAINTS=${NOINSTALLEDCONSTRAINTS-false}
6970 - GHCHEAD=${GHCHEAD-false}
7071 - travis_retry cabal update -v
7172 - "sed -i.bak 's/^jobs:/-- jobs:/' ${HOME}/.cabal/config"
7273 - rm -fv cabal.project cabal.project.local
7374 - "if [ $HCNUMVER -ge 70800 ]; then sed -i.bak 's/-- ghc-options:.*/ghc-options: -j2/' ${HOME}/.cabal/config; fi"
7475 - grep -Ev -- '^\s*--' ${HOME}/.cabal/config | grep -Ev '^\s*$'
75− - "printf 'packages: \"servant\" \"servant-client\" \"servant-client-core\" \"servant-docs\" \"servant-foreign\" \"servant-server\" \"doc/tutorial\" \"doc/cookbook/basic-auth\" \"doc/cookbook/db-postgres-pool\" \"doc/cookbook/db-sqlite-simple\" \"doc/cookbook/https\" \"doc/cookbook/pagination\" \"doc/cookbook/structuring-apis\" \"doc/cookbook/using-custom-monad\"\\n' > cabal.project"
76+ - "printf 'packages: \"servant\" \"servant-client\" \"servant-client-core\" \"servant-docs\" \"servant-foreign\" \"servant-server\" \"doc/tutorial\" \"doc/cookbook/basic-auth\" \"doc/cookbook/db-postgres-pool\" \"doc/cookbook/db-sqlite-simple\" \"doc/cookbook/file-upload\" \"doc/cookbook/https\" \"doc/cookbook/jwt-and-basic-auth\" \"doc/cookbook/pagination\" \"doc/cookbook/structuring-apis\" \"doc/cookbook/using-custom-monad\"\\n' > cabal.project"
7677 - "echo 'constraints: foundation >=0.0.14,memory <0.14.12 || >0.14.12' >> cabal.project"
77− - "echo 'allow-newer: servant-auth-server:http-types,servant-auth-server:servant-server, http-media:base' >> cabal.project"
78− - cat cabal.project
78+ - "echo 'allow-newer: servant-auth-server:http-types,servant-auth-server:servant-server, servant-pagination:servant,servant-pagination:servant-server' >> cabal.project"
79+ - touch cabal.project.local
80+ - "if ! $NOINSTALLEDCONSTRAINTS; then for pkg in $($HCPKG list --simple-output); do echo $pkg | sed 's/^/constraints: /' | sed 's/-[^-]*$/ installed/' >> cabal.project.local; done; fi"
81+ - cat cabal.project || true
82+ - cat cabal.project.local || true
7983 - if [ -f "servant/configure.ac" ]; then
8084 (cd "servant" && autoreconf -i);
8185 fi
@@ -106,9 +110,15 @@ install:
106110 - if [ -f "doc/cookbook/db-sqlite-simple/configure.ac" ]; then
107111 (cd "doc/cookbook/db-sqlite-simple" && autoreconf -i);
108112 fi
113+ - if [ -f "doc/cookbook/file-upload/configure.ac" ]; then
114+ (cd "doc/cookbook/file-upload" && autoreconf -i);
115+ fi
109116 - if [ -f "doc/cookbook/https/configure.ac" ]; then
110117 (cd "doc/cookbook/https" && autoreconf -i);
111118 fi
119+ - if [ -f "doc/cookbook/jwt-and-basic-auth/configure.ac" ]; then
120+ (cd "doc/cookbook/jwt-and-basic-auth" && autoreconf -i);
121+ fi
112122 - if [ -f "doc/cookbook/pagination/configure.ac" ]; then
113123 (cd "doc/cookbook/pagination" && autoreconf -i);
114124 fi
@@ -119,7 +129,7 @@ install:
119129 (cd "doc/cookbook/using-custom-monad" && autoreconf -i);
120130 fi
121131 - rm -f cabal.project.freeze
122− - rm -rf .ghc.environment.* "servant"/dist "servant-client"/dist "servant-client-core"/dist "servant-docs"/dist "servant-foreign"/dist "servant-server"/dist "doc/tutorial"/dist "doc/cookbook/basic-auth"/dist "doc/cookbook/db-postgres-pool"/dist "doc/cookbook/db-sqlite-simple"/dist "doc/cookbook/https"/dist "doc/cookbook/pagination"/dist "doc/cookbook/structuring-apis"/dist "doc/cookbook/using-custom-monad"/dist
132+ - rm -rf .ghc.environment.* "servant"/dist "servant-client"/dist "servant-client-core"/dist "servant-docs"/dist "servant-foreign"/dist "servant-server"/dist "doc/tutorial"/dist "doc/cookbook/basic-auth"/dist "doc/cookbook/db-postgres-pool"/dist "doc/cookbook/db-sqlite-simple"/dist "doc/cookbook/file-upload"/dist "doc/cookbook/https"/dist "doc/cookbook/jwt-and-basic-auth"/dist "doc/cookbook/pagination"/dist "doc/cookbook/structuring-apis"/dist "doc/cookbook/using-custom-monad"/dist
123133 - DISTDIR=$(mktemp -d /tmp/dist-test.XXXX)
124134
125135# Here starts the actual work to be performed for the package under test;
@@ -137,22 +147,26 @@ script:
137147 - (cd "doc/cookbook/basic-auth" && cabal sdist)
138148 - (cd "doc/cookbook/db-postgres-pool" && cabal sdist)
139149 - (cd "doc/cookbook/db-sqlite-simple" && cabal sdist)
150+ - (cd "doc/cookbook/file-upload" && cabal sdist)
140151 - (cd "doc/cookbook/https" && cabal sdist)
152+ - (cd "doc/cookbook/jwt-and-basic-auth" && cabal sdist)
141153 - (cd "doc/cookbook/pagination" && cabal sdist)
142154 - (cd "doc/cookbook/structuring-apis" && cabal sdist)
143155 - (cd "doc/cookbook/using-custom-monad" && cabal sdist)
144156 - echo -en 'travis_fold:end:sdist\\r'
145157 - echo Unpacking... && echo -en 'travis_fold:start:unpack\\r'
146− - mv "servant"/dist/servant-*.tar.gz "servant-client"/dist/servant-client-*.tar.gz "servant-client-core"/dist/servant-client-core-*.tar.gz "servant-docs"/dist/servant-docs-*.tar.gz "servant-foreign"/dist/servant-foreign-*.tar.gz "servant-server"/dist/servant-server-*.tar.gz "doc/tutorial"/dist/tutorial-*.tar.gz "doc/cookbook/basic-auth"/dist/cookbook-basic-auth-*.tar.gz "doc/cookbook/db-postgres-pool"/dist/cookbook-db-postgres-pool-*.tar.gz "doc/cookbook/db-sqlite-simple"/dist/cookbook-db-sqlite-simple-*.tar.gz "doc/cookbook/https"/dist/cookbook-https-*.tar.gz "doc/cookbook/pagination"/dist/cookbook-pagination-*.tar.gz "doc/cookbook/structuring-apis"/dist/cookbook-structuring-apis-*.tar.gz "doc/cookbook/using-custom-monad"/dist/cookbook-using-custom-monad-*.tar.gz ${DISTDIR}/
158+ - mv "servant"/dist/servant-*.tar.gz "servant-client"/dist/servant-client-*.tar.gz "servant-client-core"/dist/servant-client-core-*.tar.gz "servant-docs"/dist/servant-docs-*.tar.gz "servant-foreign"/dist/servant-foreign-*.tar.gz "servant-server"/dist/servant-server-*.tar.gz "doc/tutorial"/dist/tutorial-*.tar.gz "doc/cookbook/basic-auth"/dist/cookbook-basic-auth-*.tar.gz "doc/cookbook/db-postgres-pool"/dist/cookbook-db-postgres-pool-*.tar.gz "doc/cookbook/db-sqlite-simple"/dist/cookbook-db-sqlite-simple-*.tar.gz "doc/cookbook/file-upload"/dist/cookbook-file-upload-*.tar.gz "doc/cookbook/https"/dist/cookbook-https-*.tar.gz "doc/cookbook/jwt-and-basic-auth"/dist/cookbook-jwt-and-basic-auth-*.tar.gz "doc/cookbook/pagination"/dist/cookbook-pagination-*.tar.gz "doc/cookbook/structuring-apis"/dist/cookbook-structuring-apis-*.tar.gz "doc/cookbook/using-custom-monad"/dist/cookbook-using-custom-monad-*.tar.gz ${DISTDIR}/
147159 - cd ${DISTDIR} || false
148160 - find . -maxdepth 1 -name '*.tar.gz' -exec tar -xvf '{}' \;
149− - "printf 'packages: servant-*/*.cabal servant-client-*/*.cabal servant-client-core-*/*.cabal servant-docs-*/*.cabal servant-foreign-*/*.cabal servant-server-*/*.cabal tutorial-*/*.cabal cookbook-basic-auth-*/*.cabal cookbook-db-postgres-pool-*/*.cabal cookbook-db-sqlite-simple-*/*.cabal cookbook-https-*/*.cabal cookbook-pagination-*/*.cabal cookbook-structuring-apis-*/*.cabal cookbook-using-custom-monad-*/*.cabal\\n' > cabal.project"
161+ - "printf 'packages: servant-*/*.cabal servant-client-*/*.cabal servant-client-core-*/*.cabal servant-docs-*/*.cabal servant-foreign-*/*.cabal servant-server-*/*.cabal tutorial-*/*.cabal cookbook-basic-auth-*/*.cabal cookbook-db-postgres-pool-*/*.cabal cookbook-db-sqlite-simple-*/*.cabal cookbook-file-upload-*/*.cabal cookbook-https-*/*.cabal cookbook-jwt-and-basic-auth-*/*.cabal cookbook-pagination-*/*.cabal cookbook-structuring-apis-*/*.cabal cookbook-using-custom-monad-*/*.cabal\\n' > cabal.project"
150162 - "echo 'constraints: foundation >=0.0.14,memory <0.14.12 || >0.14.12' >> cabal.project"
151− - "echo 'allow-newer: servant-auth-server:http-types,servant-auth-server:servant-server, http-media:base' >> cabal.project"
152− - cat cabal.project
163+ - "echo 'allow-newer: servant-auth-server:http-types,servant-auth-server:servant-server, servant-pagination:servant,servant-pagination:servant-server' >> cabal.project"
164+ - touch cabal.project.local
165+ - "if ! $NOINSTALLEDCONSTRAINTS; then for pkg in $($HCPKG list --simple-output); do echo $pkg | sed 's/^/constraints: /' | sed 's/-[^-]*$/ installed/' >> cabal.project.local; done; fi"
166+ - cat cabal.project || true
167+ - cat cabal.project.local || true
153168 - echo -en 'travis_fold:end:unpack\\r'
154169
155−
156170 - echo Building with tests and benchmarks... && echo -en 'travis_fold:start:build-everything\\r'
157171 # build & run tests, build benchmarks
158172 - cabal new-build -w ${HC} ${TEST} ${BENCH} all
@@ -165,5 +179,10 @@ script:
165179 - if $HADDOCK; then cabal new-haddock -w ${HC} ${TEST} ${BENCH} all; else echo "Skipping haddock generation";fi
166180
167181 - echo -en 'travis_fold:end:haddock\\r'
182+ - echo Building without installed constraints for packages in global-db... && echo -en 'travis_fold:start:build-installed\\r'
183+ # Build without installed constraints for packages in global-db
184+ - if $UNCONSTRAINED; then rm -f cabal.project.local; echo cabal new-build -w ${HC} --disable-tests --disable-benchmarks all; else echo "Not building without installed constraints"; fi
185+
186+ - echo -en 'travis_fold:end:build-installed\\r'
168187# REGENDATA ["--config=cabal.make-travis-yml","--output=.travis.yml","cabal.project"]
169188# EOF
cabal.make-travis-yml+1−1
@@ -1,5 +1,5 @@
11folds: all-but-test
2−branches: master release-0.12
2+branches: master release-0.14
33
44-- We have inplace packages (servant-js) so we skip installing dependencies in a separate step
55install-dependencies-step: False
cabal.project+2−8
@@ -11,11 +11,9 @@ packages: servant/
1111 doc/cookbook/basic-auth
1212 doc/cookbook/db-postgres-pool
1313 doc/cookbook/db-sqlite-simple
14− -- MkLink changed
15− -- doc/cookbook/file-upload
14+ doc/cookbook/file-upload
1615 doc/cookbook/https
17− -- servant-auth-* doesn't support GHC-8.4
18− -- doc/cookbook/jwt-and-basic-auth
16+ doc/cookbook/jwt-and-basic-auth
1917 doc/cookbook/pagination
2018 doc/cookbook/structuring-apis
2119 doc/cookbook/using-custom-monad
@@ -30,9 +28,5 @@ constraints:
3028 memory <0.14.12 || >0.14.12
3129
3230allow-newer:
33− http-media:base
34−
35−allow-newer:
36− servant-js:servant,
3731 servant-pagination:servant,
3832 servant-pagination:servant-server
doc/index.rst+11−9
@@ -3,22 +3,24 @@ servant – A Type-Level Web DSL
33
44.. image:: https://raw.githubusercontent.com/haskell-servant/servant/master/servant.png
55
6−**servant** is a set of packages for declaring web APIs at the type-level and
7−then using those API specifications to:
6+**servant** is a set of Haskell libraries for writing *type-safe* web
7+applications but also *deriving* clients (in Haskell and other languages) or
8+generating documentation for them, and more.
89
9−- write servers (this part of **servant** can be considered a web framework),
10−- obtain client functions (in haskell),
11−- generate client functions for other programming languages,
12−- generate documentation for your web applications
13−- and more...
10+This is achieved by taking as input a description of the web API
11+as a Haskell type. Servant is then able to check that your server-side request
12+handlers indeed implement your web API faithfully, or to automatically derive
13+Haskell functions that can hit a web application that implements this API,
14+generate a Swagger description or code for client functions in some other
15+languages directly.
1416
15−All in a type-safe manner.
17+If you would like to learn more, click the tutorial link below.
1618
1719.. toctree::
1820 :maxdepth: 2
1921
20− introduction.rst
2122 tutorial/index.rst
2223 cookbook/index.rst
2324 examples.md
2425 links.rst
26+ principles.rst
doc/introduction.rst → doc/principles.rstrenamed+2−2
@@ -1,5 +1,5 @@
1−Introduction
2−------------
1+Principles
2+----------
33
44**servant** has the following guiding principles:
55
doc/tutorial/Javascript.lhs+1−2
@@ -477,7 +477,7 @@ data AngularOptions = AngularOptions
477477 }
478478```
479479
480−# Custom function name builder
480+## Custom function name builder
481481
482482Servant comes with three name builders included:
483483
@@ -518,4 +518,3 @@ var get_books = function(q, onSuccess, onError)
518518}
519519
520520```
521−
doc/tutorial/index.rst+21−2
@@ -3,9 +3,28 @@ Tutorial
33
44This is an introductory tutorial to **servant**. Whilst browsing is fine, it makes more sense if you read the sections in order, or at least read the first section before anything else.
55
6−(Any comments, issues or feedback about the tutorial can be submitted
7−to `servant's issue tracker <http://github.com/haskell-servant/servant/issues>`_.)
6+Any comments, issues or feedback about the tutorial can be submitted
7+to `servant's issue tracker <http://github.com/haskell-servant/servant/issues>`_.
88
9+In fact, the whole tutorial is a `cabal <https://cabal.readthedocs.io/en/latest/>`_
10+project and can be built and played with locally as follows:
11+
12+.. code-block:: bash
13+
14+ $ git clone https://github.com/haskell-servant/servant.git
15+ $ cd servant
16+ # build
17+ $ cabal new-build tutorial
18+ # load in ghci to play with it
19+ $ cabal new-repl tutorial
20+
21+The code can be found in the `*.lhs` files under `doc/tutorial/` in the
22+repository. Feel free to edit it while you're reading this documentation and
23+see the effect of your changes.
24+
25+`Nix <https://nixos.org/nix/>`_ users should feel free to take a look at
26+the `nix/shell.nix` file in the repository and use it to provision a suitable
27+environment to build and run the examples.
928
1029.. toctree::
1130 :maxdepth: 1
servant-client-core/CHANGELOG.md+42−2
@@ -4,10 +4,50 @@
440.14
55----
66
7−- Add a `hoistClientMonad` method to the `HasClient` typeclass, for
8− changing the monad in which client functions run.
7+- `Stream` takes a status code argument
8+
9+ ```diff
10+ -Stream method framing ctype a
11+ +Stream method status framing ctype a
12+ ```
13+
14+ ([#966](https://github.com/haskell-servant/servant/pull/966)
15+ [#972](https://github.com/haskell-servant/servant/pull/972))
16+
17+- `ToStreamGenerator` definition changed, so it's possible to write an instance
18+ for conduits.
19+
20+ ```diff
21+ -class ToStreamGenerator f a where
22+ - toStreamGenerator :: f a -> StreamGenerator a
23+ +class ToStreamGenerator a b | a -> b where
24+ + toStreamGenerator :: a -> StreamGenerator b
25+ ```
26+
27+ ([#959](https://github.com/haskell-servant/servant/pull/959))
28+
29+- Added `NoFraming` streaming strategy
30+ ([#959](https://github.com/haskell-servant/servant/pull/959))
31+
32+- *servant-client-core* Free `Client` implementation.
33+ Useful for testing `HasClient` instances.
34+ ([#920](https://github.com/haskell-servant/servant/pull/920))
35+
36+- *servant-client-core* Add `hoistClient` to `HasClient`.
37+ Just like `hoistServer` allows us to change the monad in which request handlers
38+ of a web application live in, we also have `hoistClient` for changing the monad
39+ in which *client functions* live.
40+ Read [tutorial section for more information](https://haskell-servant.readthedocs.io/en/release-0.14/tutorial/Client.html#changing-the-monad-the-client-functions-live-in).
941 ([#936](https://github.com/haskell-servant/servant/pull/936))
1042
43+ iF you have own combinators, you'll need to define a new method of
44+ `HasClient` class, for example:
45+
46+ ```haskell
47+ type Client m (MyCombinator :> api) = MyValue :> Client m api
48+ hoistClientMonad pm _ nt cl = hoistClientMonad pm (Proxy :: Proxy api) nt . cl
49+ ```
50+
11510.13.0.1
1252--------
1353
servant-client-core/servant-client-core.cabal+12−13
@@ -51,12 +51,11 @@ library
5151 base >= 4.7 && < 4.12
5252 , bytestring >= 0.10.4.0 && < 0.11
5353 , containers >= 0.5.5.1 && < 0.6
54− , mtl >= 2.1 && < 2.3
5554 , text >= 1.2.3.0 && < 1.3
5655
5756 if !impl(ghc >= 8.0)
5857 build-depends:
59− semigroups >=0.18.3 && <0.19
58+ semigroups >=0.18.4 && <0.19
6059
6160 -- Servant dependencies
6261 build-depends:
@@ -65,16 +64,16 @@ library
6564 -- Other dependencies: Lower bound around what is in the latest Stackage LTS.
6665 -- Here can be exceptions if we really need features from the newer versions.
6766 build-depends:
68− base-compat >= 0.9.3 && < 0.11
67+ base-compat >= 0.10.1 && < 0.11
6968 , base64-bytestring >= 1.0.0.1 && < 1.1
70− , exceptions >= 0.8.3 && < 0.11
71− , free >= 5.0.1 && < 5.1
72− , generics-sop >= 0.3.1.0 && < 0.4
73− , http-api-data >= 0.3.7.1 && < 0.4
74− , http-media >= 0.7.1.1 && < 0.8
75− , http-types >= 0.12 && < 0.13
69+ , exceptions >= 0.10.0 && < 0.11
70+ , free >= 5.0.2 && < 5.1
71+ , generics-sop >= 0.3.2.0 && < 0.4
72+ , http-api-data >= 0.3.8.1 && < 0.4
73+ , http-media >= 0.7.1.2 && < 0.8
74+ , http-types >= 0.12.1 && < 0.13
7675 , network-uri >= 2.6.1.0 && < 2.7
77− , safe >= 0.3.15 && < 0.4
76+ , safe >= 0.3.17 && < 0.4
7877
7978 hs-source-dirs: src
8079 default-language: Haskell2010
@@ -99,8 +98,8 @@ test-suite spec
9998 -- Additonal dependencies
10099 build-depends:
101100 deepseq >= 1.3.0.2 && <1.5
102− , hspec >= 2.4.4 && <2.6
103− , QuickCheck >= 2.10.1 && < 2.12
101+ , hspec >= 2.4.1 && <2.6
102+ , QuickCheck >= 2.11.3 && < 2.12
104103
105104 build-tool-depends:
106− hspec-discover:hspec-discover >= 2.4.4 && <2.6
105+ hspec-discover:hspec-discover >= 2.5.1 && <2.6
servant-client-ghcjs/servant-client-ghcjs.cabal+1−1
@@ -42,7 +42,7 @@ library
4242 , http-types >= 0.12 && < 0.13
4343 , monad-control >= 1.0.0.4 && < 1.1
4444 , mtl >= 2.1 && < 2.3
45− , semigroupoids >= 4.3 && < 5.3
45+ , semigroupoids >= 4.3 && < 5.4
4646 , servant-client-core == 0.14.*
4747 , string-conversions >= 0.3 && < 0.5
4848 , transformers >= 0.3 && < 0.6
servant-client/CHANGELOG.md+37−2
@@ -4,10 +4,45 @@
440.14
55----
66
7−- Add `hoistClient` for changing the monad in which
8− client functions run.
7+- `Stream` takes a status code argument
8+
9+ ```diff
10+ -Stream method framing ctype a
11+ +Stream method status framing ctype a
12+ ```
13+
14+ ([#966](https://github.com/haskell-servant/servant/pull/966)
15+ [#972](https://github.com/haskell-servant/servant/pull/972))
16+
17+- `ToStreamGenerator` definition changed, so it's possible to write an instance
18+ for conduits.
19+
20+ ```diff
21+ -class ToStreamGenerator f a where
22+ - toStreamGenerator :: f a -> StreamGenerator a
23+ +class ToStreamGenerator a b | a -> b where
24+ + toStreamGenerator :: a -> StreamGenerator b
25+ ```
26+
27+ ([#959](https://github.com/haskell-servant/servant/pull/959))
28+
29+- Added `NoFraming` streaming strategy
30+ ([#959](https://github.com/haskell-servant/servant/pull/959))
31+
32+- *servant-client-core* Add `hoistClient` to `HasClient`.
33+ Just like `hoistServer` allows us to change the monad in which request handlers
34+ of a web application live in, we also have `hoistClient` for changing the monad
35+ in which *client functions* live.
36+ Read [tutorial section for more information](https://haskell-servant.readthedocs.io/en/release-0.14/tutorial/Client.html#changing-the-monad-the-client-functions-live-in).
937 ([#936](https://github.com/haskell-servant/servant/pull/936))
1038
39+- *servant-client* Add more constructors to `RequestBody`, including
40+ `RequestBodyStream`.
41+ *Note:* we are looking for http-library agnostic API,
42+ so the might change again soon.
43+ Tell us which constructors are useful for you!
44+ ([#913](https://github.com/haskell-servant/servant/pull/913))
45+
11460.13.0.1
1247--------
1348
servant-client/servant-client.cabal+17−23
@@ -51,7 +51,7 @@ library
5151 , transformers >= 0.3.0.0 && < 0.6
5252
5353 if !impl(ghc >= 8.0)
54− build-depends: semigroups >=0.18.3 && <0.19
54+ build-depends: semigroups >=0.18.4 && <0.19
5555
5656 -- Servant dependencies
5757 build-depends:
@@ -60,19 +60,16 @@ library
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− aeson >= 1.2.3.0 && < 1.5
64− , base-compat >= 0.9.3 && < 0.11
65− , attoparsec >= 0.13.2.0 && < 0.14
66− , http-client >= 0.5.7.1 && < 0.6
67− , http-client-tls >= 0.3.5.1 && < 0.4
68− , http-media >= 0.7.1.1 && < 0.8
69− , http-types >= 0.12 && < 0.13
70− , exceptions >= 0.8.3 && < 0.11
71− , monad-control >= 1.0.0.4 && < 1.1
72− , semigroupoids >= 5.2.1 && < 5.3
73− , stm >= 2.4.4.1 && < 2.5
74− , transformers-base >= 0.4.4 && < 0.5
75− , transformers-compat >= 0.5.1 && < 0.7
63+ base-compat >= 0.10.1 && < 0.11
64+ , http-client >= 0.5.12 && < 0.6
65+ , http-media >= 0.7.1.2 && < 0.8
66+ , http-types >= 0.12.1 && < 0.13
67+ , exceptions >= 0.10.0 && < 0.11
68+ , monad-control >= 1.0.2.3 && < 1.1
69+ , semigroupoids >= 5.2.2 && < 5.4
70+ , stm >= 2.4.5.0 && < 2.5
71+ , transformers-base >= 0.4.5.2 && < 0.5
72+ , transformers-compat >= 0.6.2 && < 0.7
7673
7774 hs-source-dirs: src
7875 default-language: Haskell2010
@@ -97,10 +94,8 @@ test-suite spec
9794 , aeson
9895 , base-compat
9996 , bytestring
100− , containers
10197 , http-api-data
10298 , http-client
103− , http-media
10499 , http-types
105100 , mtl
106101 , servant-client
@@ -117,18 +112,16 @@ test-suite spec
117112
118113 -- Additonal dependencies
119114 build-depends:
120− deepseq >= 1.3.0.2 && < 1.5
121− , generics-sop >= 0.3.1.0 && < 0.4
122− , hspec >= 2.4.4 && < 2.6
115+ generics-sop >= 0.3.2.0 && < 0.4
116+ , hspec >= 2.5.1 && < 2.6
123117 , HUnit >= 1.6 && < 1.7
124− , random-bytestring >= 0.1 && < 0.2
125118 , network >= 2.6.3.2 && < 2.8
126119 , QuickCheck >= 2.10.1 && < 2.12
127− , servant == 0.13.*
128− , servant-server == 0.13.*
120+ , servant == 0.14.*
121+ , servant-server == 0.14.*
129122
130123 build-tool-depends:
131− hspec-discover:hspec-discover >= 2.4.4 && < 2.6
124+ hspec-discover:hspec-discover >= 2.5.1 && < 2.6
132125
133126test-suite readme
134127 type: exitcode-stdio-1.0
@@ -136,3 +129,4 @@ test-suite readme
136129 build-depends: base, servant, http-client, text, servant-client, markdown-unlit
137130 build-tool-depends: markdown-unlit:markdown-unlit
138131 ghc-options: -pgmL markdown-unlit
132+ default-language: Haskell2010
servant-client/test/Servant/StreamSpec.hs+2
@@ -130,6 +130,7 @@ streamSpec = beforeAll (CS.startWaiApp server) $ afterAll CS.endWaiApp $ do
130130 jrb = Just (Right bob)
131131 runResultStream res `shouldReturn` (jra, jrb, jra, Nothing)
132132
133+{-
133134 it "streams in constant memory" $ \(_, baseUrl) -> do
134135 Right (ResultStream res) <- runClient getGetALot baseUrl
135136 let consumeNChunks n = replicateM_ n (res void)
@@ -140,6 +141,7 @@ streamSpec = beforeAll (CS.startWaiApp server) $ afterAll CS.endWaiApp $ do
140141 memUsed <- currentBytesUsed <$> getGCStats
141142#endif
142143 memUsed `shouldSatisfy` (< megabytes 22)
144+-}
143145
144146megabytes :: Num a => a -> a
145147megabytes n = n * (1000 ^ (2 :: Int))
servant-docs/servant-docs.cabal+1−1
@@ -64,7 +64,7 @@ library
6464 , case-insensitive >= 1.2.0.10 && < 1.3
6565 , control-monad-omega >= 0.3.1 && < 0.4
6666 , hashable >= 1.2.6.1 && < 1.3
67− , http-media >= 0.7.1.1 && < 0.8
67+ , http-media >= 0.7.0 && < 0.8
6868 , http-types >= 0.12 && < 0.13
6969 , lens >= 4.15.4 && < 4.17
7070 , string-conversions >= 0.4.0.1 && < 0.5
servant-foreign/servant-foreign.cabal+1
@@ -1,5 +1,6 @@
11name: servant-foreign
22version: 0.11.1
3+x-revision: 2
34synopsis: Helpers for generating clients for servant APIs in any programming language
45description:
56 Helper types and functions for generating client functions for servant APIs in any programming language
servant-server/CHANGELOG.md+46
@@ -1,6 +1,52 @@
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+0.14
5+----
6+
7+- `Stream` takes a status code argument
8+
9+ ```diff
10+ -Stream method framing ctype a
11+ +Stream method status framing ctype a
12+ ```
13+
14+ ([#966](https://github.com/haskell-servant/servant/pull/966)
15+ [#972](https://github.com/haskell-servant/servant/pull/972))
16+
17+- `ToStreamGenerator` definition changed, so it's possible to write an instance
18+ for conduits.
19+
20+ ```diff
21+ -class ToStreamGenerator f a where
22+ - toStreamGenerator :: f a -> StreamGenerator a
23+ +class ToStreamGenerator a b | a -> b where
24+ + toStreamGenerator :: a -> StreamGenerator b
25+ ```
26+
27+ ([#959](https://github.com/haskell-servant/servant/pull/959))
28+
29+- Added `NoFraming` streaming strategy
30+ ([#959](https://github.com/haskell-servant/servant/pull/959))
31+
32+- *servant-server* File serving in polymorphic monad.
33+ i.e. Generalised types of `serveDirectoryFileServer` etc functions in
34+ `Servant.Utils.StaticFiles`
35+ ([#953](https://github.com/haskell-servant/servant/pull/953))
36+
37+- *servant-server* `ReqBody` content type check is recoverable.
38+ This allows writing APIs like:
39+
40+ ```haskell
41+ ReqBody '[JSON] Int :> Post '[PlainText] Int
42+ :<|> ReqBody '[PlainText] Int :> Post '[PlainText] Int
43+ ```
44+
45+ which is useful when handlers are subtly different,
46+ for example may do less work.
47+ ([#937](https://github.com/haskell-servant/servant/pull/937))
48+
49+
4500.13.0.1
551--------
652
servant-server/servant-server.cabal+23−31
@@ -40,7 +40,7 @@ custom-setup
4040 setup-depends:
4141 base >= 4 && <5,
4242 Cabal,
43− cabal-doctest >= 1.0.1 && <1.1
43+ cabal-doctest >= 1.0.6 && <1.1
4444
4545library
4646 exposed-modules:
@@ -71,7 +71,7 @@ library
7171
7272 if !impl(ghc >= 8.0)
7373 build-depends:
74− semigroups >= 0.18.3 && < 0.19
74+ semigroups >= 0.18.4 && < 0.19
7575
7676 -- Servant dependencies
7777 build-depends:
@@ -80,28 +80,22 @@ library
8080 -- Other dependencies: Lower bound around what is in the latest Stackage LTS.
8181 -- Here can be exceptions if we really need features from the newer versions.
8282 build-depends:
83− aeson >= 1.2.3.0 && < 1.5
84− , base-compat >= 0.9.3 && < 0.11
85− , attoparsec >= 0.13.2.0 && < 0.14
83+ base-compat >= 0.10.1 && < 0.11
8684 , base64-bytestring >= 1.0.0.1 && < 1.1
87− , exceptions >= 0.8.3 && < 0.11
88− , http-api-data >= 0.3.7.1 && < 0.4
89− , http-media >= 0.7.1.1 && < 0.8
90− , http-types >= 0.12 && < 0.13
85+ , exceptions >= 0.10.0 && < 0.11
86+ , http-api-data >= 0.3.8.1 && < 0.4
87+ , http-media >= 0.7.1.2 && < 0.8
88+ , http-types >= 0.12.1 && < 0.13
9189 , network-uri >= 2.6.1.0 && < 2.7
92− , monad-control >= 1.0.0.4 && < 1.1
93− , network >= 2.6.3.2 && < 2.8
94− , safe >= 0.3.15 && < 0.4
95− , split >= 0.2.3.2 && < 0.3
90+ , monad-control >= 1.0.2.3 && < 1.1
91+ , network >= 2.6.3.5 && < 2.8
9692 , string-conversions >= 0.4.0.1 && < 0.5
97− , system-filepath >= 0.4 && < 0.5
98− , resourcet >= 1.1.9 && < 1.3
93+ , resourcet >= 1.1.11 && < 1.3
9994 , tagged >= 0.8.5 && < 0.9
10095 , transformers-base >= 0.4.4 && < 0.5
101− , transformers-compat >= 0.5.1 && < 0.7
96+ , transformers-compat >= 0.6.2 && < 0.7
10297 , wai >= 3.2.1.1 && < 3.3
10398 , wai-app-static >= 3.1.6.1 && < 3.2
104− , warp >= 3.2.13 && < 3.3
10599 , word8 >= 0.1.3 && < 0.2
106100
107101 hs-source-dirs: src
@@ -120,11 +114,13 @@ executable greet
120114 base
121115 , servant
122116 , servant-server
123− , aeson
124− , warp
125117 , wai
126118 , text
127119
120+ build-depends:
121+ aeson >= 1.3.1.1 && < 1.5
122+ , warp >= 3.2.13 && < 3.3
123+
128124test-suite spec
129125 type: exitcode-stdio-1.0
130126 ghc-options: -Wall
@@ -148,13 +144,10 @@ test-suite spec
148144 build-depends:
149145 base
150146 , base-compat
151− , aeson
152147 , base64-bytestring
153148 , bytestring
154− , exceptions
155149 , http-types
156150 , mtl
157− , network
158151 , resourcet
159152 , safe
160153 , servant
@@ -164,27 +157,26 @@ test-suite spec
164157 , transformers
165158 , transformers-compat
166159 , wai
167− , warp
168160
169161 -- Additonal dependencies
170162 build-depends:
171− directory >= 1.2.1.0 && < 1.4
172− , hspec >= 2.4.4 && < 2.6
173− , hspec-wai >= 0.9 && < 0.10
163+ aeson >= 1.3.1.1 && < 1.5
164+ , directory >= 1.2.1.0 && < 1.4
165+ , hspec >= 2.5.1 && < 2.6
166+ , hspec-wai >= 0.9.0 && < 0.10
167+ , QuickCheck >= 2.11.3 && < 2.12
174168 , should-not-typecheck >= 2.1.0 && < 2.2
175− , parsec >= 3.1.11 && < 3.2
176− , QuickCheck >= 2.10.1 && < 2.12
169+ , temporary >= 1.3 && < 1.4
177170 , wai-extra >= 3.0.21.0 && < 3.1
178− , temporary >= 1.2.0.3 && < 1.4
179171
180172 build-tool-depends:
181− hspec-discover:hspec-discover >=2.4.4 && <2.6
173+ hspec-discover:hspec-discover >= 2.5.1 && <2.6
182174
183175test-suite doctests
184176 build-depends:
185177 base
186178 , servant-server
187− , doctest >= 0.13.0 && <0.16
179+ , doctest >= 0.15.0 && <0.17
188180 type: exitcode-stdio-1.0
189181 main-is: test/doctests.hs
190182 buildable: True
servant-server/src/Servant.hs+2−2
@@ -6,7 +6,7 @@ module Servant (
66 -- | For implementing servers for servant APIs.
77 module Servant.Server,
88 -- | Utilities on top of the servant core
9− module Servant.Utils.Links,
9+ module Servant.Links,
1010 module Servant.Utils.StaticFiles,
1111 -- | Useful re-exports
1212 Proxy(..),
@@ -17,5 +17,5 @@ import Control.Monad.Error.Class (throwError)
1717import Data.Proxy
1818import Servant.API
1919import Servant.Server
20−import Servant.Utils.Links
20+import Servant.Links
2121import Servant.Utils.StaticFiles
servant-server/src/Servant/Server/Internal/Context.hs+1−1
@@ -18,7 +18,7 @@ import GHC.TypeLits
1818-- | 'Context's are used to pass values to combinators. (They are __not__ meant
1919-- to be used to pass parameters to your handlers, i.e. they should not replace
2020-- any custom 'Control.Monad.Trans.Reader.ReaderT'-monad-stack that you're using
21−-- with 'Servant.Utils.Enter'.) If you don't use combinators that
21+-- with 'hoistServer'.) If you don't use combinators that
2222-- require any context entries, you can just use 'Servant.Server.serve' as always.
2323--
2424-- If you are using combinators that require a non-empty 'Context' you have to
servant/CHANGELOG.md+14−5
@@ -30,16 +30,25 @@
3030- Added `NoFraming` streaming strategy
3131 ([#959](https://github.com/haskell-servant/servant/pull/959))
3232
33−- *servant-client* Free `Client` implementation.
33+- *servant-client-core* Free `Client` implementation.
3434 Useful for testing `HasClient` instances.
3535 ([#920](https://github.com/haskell-servant/servant/pull/920))
3636
37−- *servant-client* Add `hoistClient` to `HasClient`.
37+- *servant-client-core* Add `hoistClient` to `HasClient`.
3838 Just like `hoistServer` allows us to change the monad in which request handlers
3939 of a web application live in, we also have `hoistClient` for changing the monad
40− in which *client functions* live. Read [tutorial section for more information](#link).
40+ in which *client functions* live.
41+ Read [tutorial section for more information](https://haskell-servant.readthedocs.io/en/release-0.14/tutorial/Client.html#changing-the-monad-the-client-functions-live-in).
4142 ([#936](https://github.com/haskell-servant/servant/pull/936))
4243
44+ iF you have own combinators, you'll need to define a new method of
45+ `HasClient` class, for example:
46+
47+ ```haskell
48+ type Client m (MyCombinator :> api) = MyValue :> Client m api
49+ hoistClientMonad pm _ nt cl = hoistClientMonad pm (Proxy :: Proxy api) nt . cl
50+ ```
51+
4352- *servant* Add `safeLink' :: (Link -> a) -> ... -> MkLink endpoint a`,
4453 which allows to create helpers returning something else than `Link`.
4554 ([#968](https://github.com/haskell-servant/servant/pull/968))
@@ -76,10 +85,10 @@
7685- Added tests or enabled tests
7786 ([#975](https://github.com/haskell-servant/servant/pull/975))
7887
79−- Add [pagination cookbook recipe](#link)
88+- Add [pagination cookbook recipe](https://haskell-servant.readthedocs.io/en/release-0.14/cookbook/pagination/Pagination.html)
8089 ([#946](https://github.com/haskell-servant/servant/pull/946))
8190
82−- Add [`servant-flatten` cookbook recipe](#link)
91+- Add [`servant-flatten` "spice" to the structuring api recipe](https://haskell-servant.readthedocs.io/en/release-0.14/cookbook/structuring-apis/StructuringApis.html)
8392 ([#929](https://github.com/haskell-servant/servant/pull/929))
8493
8594- Dependency updates
servant/servant.cabal+24−21
@@ -34,7 +34,7 @@ custom-setup
3434 setup-depends:
3535 base >= 4 && <5,
3636 Cabal,
37− cabal-doctest >= 1.0.2 && <1.1
37+ cabal-doctest >= 1.0.6 && <1.1
3838
3939library
4040 exposed-modules:
@@ -62,6 +62,10 @@ library
6262 Servant.API.Vault
6363 Servant.API.Verbs
6464 Servant.API.WithNamedContext
65+ Servant.Links
66+
67+ -- Deprecated modules, to be removed in late 2019
68+ exposed-modules:
6569 Servant.Utils.Links
6670 Servant.Utils.Enter
6771
@@ -77,25 +81,25 @@ library
7781
7882 if !impl(ghc >= 8.0)
7983 build-depends:
80− semigroups >= 0.18.3 && < 0.19
84+ semigroups >= 0.18.4 && < 0.19
8185
8286 -- Other dependencies: Lower bound around what is in the latest Stackage LTS.
8387 -- Here can be exceptions if we really need features from the newer versions.
8488 build-depends:
85− base-compat >= 0.9.3 && < 0.11
86− , aeson >= 1.2.3.0 && < 1.5
87− , attoparsec >= 0.13.2.0 && < 0.14
89+ base-compat >= 0.10.1 && < 0.11
90+ , aeson >= 1.3.1.1 && < 1.5
91+ , attoparsec >= 0.13.2.2 && < 0.14
8892 , case-insensitive >= 1.2.0.10 && < 1.3
89− , http-api-data >= 0.3.7.1 && < 0.4
90− , http-media >= 0.7.1.1 && < 0.8
91− , http-types >= 0.12 && < 0.13
93+ , http-api-data >= 0.3.8.1 && < 0.4
94+ , http-media >= 0.7.1.2 && < 0.8
95+ , http-types >= 0.12.1 && < 0.13
9296 , natural-transformation >= 0.4 && < 0.5
93− , mmorph >= 1.1.0 && < 1.2
97+ , mmorph >= 1.1.2 && < 1.2
9498 , tagged >= 0.8.5 && < 0.9
95− , singleton-bool >= 0.1.2.0 && < 0.2
99+ , singleton-bool >= 0.1.4 && < 0.2
96100 , string-conversions >= 0.4.0.1 && < 0.5
97101 , network-uri >= 2.6.1.0 && < 2.7
98− , vault >= 0.3.0.7 && < 0.4
102+ , vault >= 0.3.1.1 && < 0.4
99103
100104 hs-source-dirs: src
101105 default-language: Haskell2010
@@ -133,15 +137,14 @@ test-suite spec
133137 other-modules:
134138 Servant.API.ContentTypesSpec
135139 Servant.API.ResponseHeadersSpec
136− Servant.Utils.LinksSpec
137140 Servant.Utils.EnterSpec
141+ Servant.LinksSpec
138142
139143 -- Dependencies inherited from the library. No need to specify bounds.
140144 build-depends:
141145 base
142146 , base-compat
143147 , aeson
144− , attoparsec
145148 , bytestring
146149 , servant
147150 , string-conversions
@@ -153,23 +156,23 @@ test-suite spec
153156
154157 -- Additonal dependencies
155158 build-depends:
156− aeson-compat >= 0.3.3 && < 0.4
157− , hspec >= 2.4.4 && < 2.6
158− , QuickCheck >= 2.10.1 && < 2.12
159− , quickcheck-instances >= 0.3.16 && < 0.4
159+ aeson-compat >= 0.3.7.1 && < 0.4
160+ , hspec >= 2.5.1 && < 2.6
161+ , QuickCheck >= 2.11.3 && < 2.12
162+ , quickcheck-instances >= 0.3.18 && < 0.4
160163
161164 build-tool-depends:
162− hspec-discover:hspec-discover >= 2.4.4 && < 2.6
165+ hspec-discover:hspec-discover >= 2.5.1 && < 2.6
163166
164167test-suite doctests
165168 build-depends:
166169 base
167170 , servant
168− , doctest >= 0.13.0 && <0.16
171+ , doctest >= 0.15.0 && <0.17
169172
170173 -- We test Links failure with doctest, so we need extra dependencies
171174 build-depends:
172− hspec >= 2.4.4 && < 2.6
175+ hspec >= 2.5.1 && < 2.6
173176
174177 type: exitcode-stdio-1.0
175178 main-is: test/doctests.hs
@@ -180,4 +183,4 @@ test-suite doctests
180183 x-doctest-options: -fdiagnostics-color=never
181184 include-dirs: include
182185 x-doctest-source-dirs: test
183− x-doctest-modules: Servant.Utils.LinksSpec
186+ x-doctest-modules: Servant.LinksSpec
servant/src/Servant/API.hs+3−3
@@ -63,8 +63,8 @@ module Servant.API (
6363 module Servant.API.Experimental.Auth,
6464 -- | General Authentication
6565
66− -- * Utilities
67− module Servant.Utils.Links,
66+ -- * Links
67+ module Servant.Links,
6868 -- | Type-safe internal URIs
6969
7070 -- * Re-exports
@@ -134,7 +134,7 @@ import Servant.API.Verbs
134134 ReflectMethod (reflectMethod), StdMethod (..), Verb)
135135import Servant.API.WithNamedContext
136136 (WithNamedContext)
137−import Servant.Utils.Links
137+import Servant.Links
138138 (HasLink (..), IsElem, IsElem', Link, URI (..), safeLink)
139139import Web.HttpApiData
140140 (FromHttpApiData (..), ToHttpApiData (..))
servant/src/Servant/API/Verbs.hs+1−1
@@ -58,7 +58,7 @@ type Patch = Verb 'PATCH 200
5858--
5959-- If the resource cannot be created immediately, use 'PostAccepted'.
6060--
61−-- Consider using 'Servant.Utils.Links.safeLink' for the @Location@ header
61+-- Consider using 'Servant.Links.safeLink' for the @Location@ header
6262-- field.
6363
6464-- | 'POST' with 201 status code.
servant/src/Servant/Links.hsnew file+487
@@ -0,0 +1,487 @@
1+{-# LANGUAGE ConstraintKinds #-}
2+{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE FlexibleContexts #-}
4+{-# LANGUAGE FlexibleInstances #-}
5+{-# LANGUAGE FunctionalDependencies #-}
6+{-# LANGUAGE PolyKinds #-}
7+{-# LANGUAGE ScopedTypeVariables #-}
8+{-# LANGUAGE TypeFamilies #-}
9+{-# LANGUAGE TypeOperators #-}
10+{-# LANGUAGE UndecidableInstances #-}
11+{-# OPTIONS_HADDOCK not-home #-}
12+
13+-- | Type safe generation of internal links.
14+--
15+-- Given an API with a few endpoints:
16+--
17+-- >>> :set -XDataKinds -XTypeFamilies -XTypeOperators
18+-- >>> import Servant.API
19+-- >>> import Servant.Links
20+-- >>> import Data.Proxy
21+-- >>>
22+-- >>> type Hello = "hello" :> Get '[JSON] Int
23+-- >>> type Bye = "bye" :> QueryParam "name" String :> Delete '[JSON] NoContent
24+-- >>> type API = Hello :<|> Bye
25+-- >>> let api = Proxy :: Proxy API
26+--
27+-- It is possible to generate links that are guaranteed to be within 'API' with
28+-- 'safeLink'. The first argument to 'safeLink' is a type representing the API
29+-- you would like to restrict links to. The second argument is the destination
30+-- endpoint you would like the link to point to, this will need to end with a
31+-- verb like GET or POST. Further arguments may be required depending on the
32+-- type of the endpoint. If everything lines up you will get a 'Link' out the
33+-- other end.
34+--
35+-- You may omit 'QueryParam's and the like should you not want to provide them,
36+-- but types which form part of the URL path like 'Capture' must be included.
37+-- The reason you may want to omit 'QueryParam's is that safeLink is a bit
38+-- magical: if parameters are included that could take input it will return a
39+-- function that accepts that input and generates a link. This is best shown
40+-- with an example. Here, a link is generated with no parameters:
41+--
42+-- >>> let hello = Proxy :: Proxy ("hello" :> Get '[JSON] Int)
43+-- >>> toUrlPiece (safeLink api hello :: Link)
44+-- "hello"
45+--
46+-- If the API has an endpoint with parameters then we can generate links with
47+-- or without those:
48+--
49+-- >>> let with = Proxy :: Proxy ("bye" :> QueryParam "name" String :> Delete '[JSON] NoContent)
50+-- >>> toUrlPiece $ safeLink api with (Just "Hubert")
51+-- "bye?name=Hubert"
52+--
53+-- >>> let without = Proxy :: Proxy ("bye" :> Delete '[JSON] NoContent)
54+-- >>> toUrlPiece $ safeLink api without
55+-- "bye"
56+--
57+-- If you would like create a helper for generating links only within that API,
58+-- you can partially apply safeLink if you specify a correct type signature
59+-- like so:
60+--
61+-- >>> :set -XConstraintKinds
62+-- >>> :{
63+-- >>> let apiLink :: (IsElem endpoint API, HasLink endpoint)
64+-- >>> => Proxy endpoint -> MkLink endpoint Link
65+-- >>> apiLink = safeLink api
66+-- >>> :}
67+--
68+-- `safeLink'` allows to make specialise the output:
69+--
70+-- >>> safeLink' toUrlPiece api without
71+-- "bye"
72+--
73+-- >>> :{
74+-- >>> let apiTextLink :: (IsElem endpoint API, HasLink endpoint)
75+-- >>> => Proxy endpoint -> MkLink endpoint Text
76+-- >>> apiTextLink = safeLink' toUrlPiece api
77+-- >>> :}
78+--
79+-- >>> apiTextLink without
80+-- "bye"
81+--
82+-- Attempting to construct a link to an endpoint that does not exist in api
83+-- will result in a type error like this:
84+--
85+-- >>> let bad_link = Proxy :: Proxy ("hello" :> Delete '[JSON] NoContent)
86+-- >>> safeLink api bad_link
87+-- ...
88+-- ...Could not deduce...
89+-- ...
90+--
91+-- This error is essentially saying that the type family couldn't find
92+-- bad_link under api after trying the open (but empty) type family
93+-- `IsElem'` as a last resort.
94+module Servant.Links (
95+ module Servant.API.TypeLevel,
96+
97+ -- * Building and using safe links
98+ --
99+ -- | Note that 'URI' is from the "Network.URI" module in the @network-uri@ package.
100+ safeLink
101+ , safeLink'
102+ , allLinks
103+ , allLinks'
104+ , URI(..)
105+ -- * Adding custom types
106+ , HasLink(..)
107+ , Link
108+ , linkURI
109+ , linkURI'
110+ , LinkArrayElementStyle (..)
111+ -- ** Link accessors
112+ , Param (..)
113+ , linkSegments
114+ , linkQueryParams
115+) where
116+
117+import Data.List
118+import Data.Proxy
119+ (Proxy (..))
120+import Data.Semigroup
121+ ((<>))
122+import Data.Singletons.Bool
123+ (SBool (..), SBoolI (..))
124+import qualified Data.Text as Text
125+import qualified Data.Text.Encoding as TE
126+import Data.Type.Bool
127+ (If)
128+import GHC.TypeLits
129+ (KnownSymbol, symbolVal)
130+import Network.URI
131+ (URI (..), escapeURIString, isUnreserved)
132+import Prelude ()
133+import Prelude.Compat
134+
135+import Servant.API.Alternative
136+ ((:<|>) ((:<|>)))
137+import Servant.API.BasicAuth
138+ (BasicAuth)
139+import Servant.API.Capture
140+ (Capture', CaptureAll)
141+import Servant.API.Description
142+ (Description, Summary)
143+import Servant.API.Empty
144+ (EmptyAPI (..))
145+import Servant.API.Experimental.Auth
146+ (AuthProtect)
147+import Servant.API.Header
148+ (Header')
149+import Servant.API.HttpVersion
150+ (HttpVersion)
151+import Servant.API.IsSecure
152+ (IsSecure)
153+import Servant.API.Modifiers
154+ (FoldRequired)
155+import Servant.API.QueryParam
156+ (QueryFlag, QueryParam', QueryParams)
157+import Servant.API.Raw
158+ (Raw)
159+import Servant.API.RemoteHost
160+ (RemoteHost)
161+import Servant.API.ReqBody
162+ (ReqBody')
163+import Servant.API.Stream
164+ (Stream)
165+import Servant.API.Sub
166+ (type (:>))
167+import Servant.API.TypeLevel
168+import Servant.API.Vault
169+ (Vault)
170+import Servant.API.Verbs
171+ (Verb)
172+import Servant.API.WithNamedContext
173+ (WithNamedContext)
174+import Web.HttpApiData
175+
176+-- | A safe link datatype.
177+-- The only way of constructing a 'Link' is using 'safeLink', which means any
178+-- 'Link' is guaranteed to be part of the mentioned API.
179+data Link = Link
180+ { _segments :: [Escaped]
181+ , _queryParams :: [Param]
182+ } deriving Show
183+
184+newtype Escaped = Escaped String
185+
186+escaped :: String -> Escaped
187+escaped = Escaped . escapeURIString isUnreserved
188+
189+getEscaped :: Escaped -> String
190+getEscaped (Escaped s) = s
191+
192+instance Show Escaped where
193+ showsPrec d (Escaped s) = showsPrec d s
194+ show (Escaped s) = show s
195+
196+linkSegments :: Link -> [String]
197+linkSegments = map getEscaped . _segments
198+
199+linkQueryParams :: Link -> [Param]
200+linkQueryParams = _queryParams
201+
202+instance ToHttpApiData Link where
203+ toHeader = TE.encodeUtf8 . toUrlPiece
204+ toUrlPiece l =
205+ let uri = linkURI l
206+ in Text.pack $ uriPath uri ++ uriQuery uri
207+
208+-- | Query parameter.
209+data Param
210+ = SingleParam String Text.Text
211+ | ArrayElemParam String Text.Text
212+ | FlagParam String
213+ deriving Show
214+
215+addSegment :: Escaped -> Link -> Link
216+addSegment seg l = l { _segments = _segments l <> [seg] }
217+
218+addQueryParam :: Param -> Link -> Link
219+addQueryParam qp l =
220+ l { _queryParams = _queryParams l <> [qp] }
221+
222+-- | Transform 'Link' into 'URI'.
223+--
224+-- >>> type API = "something" :> Get '[JSON] Int
225+-- >>> linkURI $ safeLink (Proxy :: Proxy API) (Proxy :: Proxy API)
226+-- something
227+--
228+-- >>> type API = "sum" :> QueryParams "x" Int :> Get '[JSON] Int
229+-- >>> linkURI $ safeLink (Proxy :: Proxy API) (Proxy :: Proxy API) [1, 2, 3]
230+-- sum?x[]=1&x[]=2&x[]=3
231+--
232+-- >>> type API = "foo/bar" :> Get '[JSON] Int
233+-- >>> linkURI $ safeLink (Proxy :: Proxy API) (Proxy :: Proxy API)
234+-- foo%2Fbar
235+--
236+-- >>> type SomeRoute = "abc" :> Capture "email" String :> Put '[JSON] ()
237+-- >>> let someRoute = Proxy :: Proxy SomeRoute
238+-- >>> safeLink someRoute someRoute "test@example.com"
239+-- Link {_segments = ["abc","test%40example.com"], _queryParams = []}
240+--
241+-- >>> linkURI $ safeLink someRoute someRoute "test@example.com"
242+-- abc/test%40example.com
243+--
244+linkURI :: Link -> URI
245+linkURI = linkURI' LinkArrayElementBracket
246+
247+-- | How to encode array query elements.
248+data LinkArrayElementStyle
249+ = LinkArrayElementBracket -- ^ @foo[]=1&foo[]=2@
250+ | LinkArrayElementPlain -- ^ @foo=1&foo=2@
251+ deriving (Eq, Ord, Show, Enum, Bounded)
252+
253+-- | Configurable 'linkURI'.
254+--
255+-- >>> type API = "sum" :> QueryParams "x" Int :> Get '[JSON] Int
256+-- >>> linkURI' LinkArrayElementBracket $ safeLink (Proxy :: Proxy API) (Proxy :: Proxy API) [1, 2, 3]
257+-- sum?x[]=1&x[]=2&x[]=3
258+--
259+-- >>> linkURI' LinkArrayElementPlain $ safeLink (Proxy :: Proxy API) (Proxy :: Proxy API) [1, 2, 3]
260+-- sum?x=1&x=2&x=3
261+--
262+linkURI' :: LinkArrayElementStyle -> Link -> URI
263+linkURI' addBrackets (Link segments q_params) =
264+ URI mempty -- No scheme (relative)
265+ Nothing -- Or authority (relative)
266+ (intercalate "/" $ map getEscaped segments)
267+ (makeQueries q_params) mempty
268+ where
269+ makeQueries :: [Param] -> String
270+ makeQueries [] = ""
271+ makeQueries xs =
272+ "?" <> intercalate "&" (fmap makeQuery xs)
273+
274+ makeQuery :: Param -> String
275+ makeQuery (ArrayElemParam k v) = escape k <> style <> escape (Text.unpack v)
276+ makeQuery (SingleParam k v) = escape k <> "=" <> escape (Text.unpack v)
277+ makeQuery (FlagParam k) = escape k
278+
279+ style = case addBrackets of
280+ LinkArrayElementBracket -> "[]="
281+ LinkArrayElementPlain -> "="
282+
283+escape :: String -> String
284+escape = escapeURIString isUnreserved
285+
286+-- | Create a valid (by construction) relative URI with query params.
287+--
288+-- This function will only typecheck if `endpoint` is part of the API `api`
289+safeLink
290+ :: forall endpoint api. (IsElem endpoint api, HasLink endpoint)
291+ => Proxy api -- ^ The whole API that this endpoint is a part of
292+ -> Proxy endpoint -- ^ The API endpoint you would like to point to
293+ -> MkLink endpoint Link
294+safeLink = safeLink' id
295+
296+-- | More general 'safeLink'.
297+--
298+safeLink'
299+ :: forall endpoint api a. (IsElem endpoint api, HasLink endpoint)
300+ => (Link -> a)
301+ -> Proxy api -- ^ The whole API that this endpoint is a part of
302+ -> Proxy endpoint -- ^ The API endpoint you would like to point to
303+ -> MkLink endpoint a
304+safeLink' toA _ endpoint = toLink toA endpoint (Link mempty mempty)
305+
306+-- | Create all links in an API.
307+--
308+-- Note that the @api@ type must be restricted to the endpoints that have
309+-- valid links to them.
310+--
311+-- >>> type API = "foo" :> Capture "name" Text :> Get '[JSON] Text :<|> "bar" :> Capture "name" Int :> Get '[JSON] Double
312+-- >>> let fooLink :<|> barLink = allLinks (Proxy :: Proxy API)
313+-- >>> :t fooLink
314+-- fooLink :: Text -> Link
315+-- >>> :t barLink
316+-- barLink :: Int -> Link
317+--
318+-- Note: nested APIs don't work well with this approach
319+--
320+-- >>> :kind! MkLink (Capture "nest" Char :> (Capture "x" Int :> Get '[JSON] Int :<|> Capture "y" Double :> Get '[JSON] Double)) Link
321+-- MkLink (Capture "nest" Char :> (Capture "x" Int :> Get '[JSON] Int :<|> Capture "y" Double :> Get '[JSON] Double)) Link :: *
322+-- = Char -> (Int -> Link) :<|> (Double -> Link)
323+allLinks
324+ :: forall api. HasLink api
325+ => Proxy api
326+ -> MkLink api Link
327+allLinks = allLinks' id
328+
329+-- | More general 'allLinks'. See `safeLink'`.
330+allLinks'
331+ :: forall api a. HasLink api
332+ => (Link -> a)
333+ -> Proxy api
334+ -> MkLink api a
335+allLinks' toA api = toLink toA api (Link mempty mempty)
336+
337+-- | Construct a toLink for an endpoint.
338+class HasLink endpoint where
339+ type MkLink endpoint (a :: *)
340+ toLink
341+ :: (Link -> a)
342+ -> Proxy endpoint -- ^ The API endpoint you would like to point to
343+ -> Link
344+ -> MkLink endpoint a
345+
346+-- Naked symbol instance
347+instance (KnownSymbol sym, HasLink sub) => HasLink (sym :> sub) where
348+ type MkLink (sym :> sub) a = MkLink sub a
349+ toLink toA _ =
350+ toLink toA (Proxy :: Proxy sub) . addSegment (escaped seg)
351+ where
352+ seg = symbolVal (Proxy :: Proxy sym)
353+
354+-- QueryParam instances
355+instance (KnownSymbol sym, ToHttpApiData v, HasLink sub, SBoolI (FoldRequired mods))
356+ => HasLink (QueryParam' mods sym v :> sub)
357+ where
358+ type MkLink (QueryParam' mods sym v :> sub) a = If (FoldRequired mods) v (Maybe v) -> MkLink sub a
359+ toLink toA _ l mv =
360+ toLink toA (Proxy :: Proxy sub) $
361+ case sbool :: SBool (FoldRequired mods) of
362+ STrue -> (addQueryParam . SingleParam k . toQueryParam) mv l
363+ SFalse -> maybe id (addQueryParam . SingleParam k . toQueryParam) mv l
364+ where
365+ k :: String
366+ k = symbolVal (Proxy :: Proxy sym)
367+
368+instance (KnownSymbol sym, ToHttpApiData v, HasLink sub)
369+ => HasLink (QueryParams sym v :> sub)
370+ where
371+ type MkLink (QueryParams sym v :> sub) a = [v] -> MkLink sub a
372+ toLink toA _ l =
373+ toLink toA (Proxy :: Proxy sub) .
374+ foldl' (\l' v -> addQueryParam (ArrayElemParam k (toQueryParam v)) l') l
375+ where
376+ k = symbolVal (Proxy :: Proxy sym)
377+
378+instance (KnownSymbol sym, HasLink sub)
379+ => HasLink (QueryFlag sym :> sub)
380+ where
381+ type MkLink (QueryFlag sym :> sub) a = Bool -> MkLink sub a
382+ toLink toA _ l False =
383+ toLink toA (Proxy :: Proxy sub) l
384+ toLink toA _ l True =
385+ toLink toA (Proxy :: Proxy sub) $ addQueryParam (FlagParam k) l
386+ where
387+ k = symbolVal (Proxy :: Proxy sym)
388+
389+-- :<|> instance - Generate all links at once
390+instance (HasLink a, HasLink b) => HasLink (a :<|> b) where
391+ type MkLink (a :<|> b) r = MkLink a r :<|> MkLink b r
392+ toLink toA _ l = toLink toA (Proxy :: Proxy a) l :<|> toLink toA (Proxy :: Proxy b) l
393+
394+-- Misc instances
395+instance HasLink sub => HasLink (ReqBody' mods ct a :> sub) where
396+ type MkLink (ReqBody' mods ct a :> sub) r = MkLink sub r
397+ toLink toA _ = toLink toA (Proxy :: Proxy sub)
398+
399+instance (ToHttpApiData v, HasLink sub)
400+ => HasLink (Capture' mods sym v :> sub)
401+ where
402+ type MkLink (Capture' mods sym v :> sub) a = v -> MkLink sub a
403+ toLink toA _ l v =
404+ toLink toA (Proxy :: Proxy sub) $
405+ addSegment (escaped . Text.unpack $ toUrlPiece v) l
406+
407+instance (ToHttpApiData v, HasLink sub)
408+ => HasLink (CaptureAll sym v :> sub)
409+ where
410+ type MkLink (CaptureAll sym v :> sub) a = [v] -> MkLink sub a
411+ toLink toA _ l vs = toLink toA (Proxy :: Proxy sub) $
412+ foldl' (flip $ addSegment . escaped . Text.unpack . toUrlPiece) l vs
413+
414+instance HasLink sub => HasLink (Header' mods sym (a :: *) :> sub) where
415+ type MkLink (Header' mods sym a :> sub) r = MkLink sub r
416+ toLink = simpleToLink (Proxy :: Proxy sub)
417+
418+instance HasLink sub => HasLink (Vault :> sub) where
419+ type MkLink (Vault :> sub) a = MkLink sub a
420+ toLink = simpleToLink (Proxy :: Proxy sub)
421+
422+instance HasLink sub => HasLink (Description s :> sub) where
423+ type MkLink (Description s :> sub) a = MkLink sub a
424+ toLink = simpleToLink (Proxy :: Proxy sub)
425+
426+instance HasLink sub => HasLink (Summary s :> sub) where
427+ type MkLink (Summary s :> sub) a = MkLink sub a
428+ toLink = simpleToLink (Proxy :: Proxy sub)
429+
430+instance HasLink sub => HasLink (HttpVersion :> sub) where
431+ type MkLink (HttpVersion:> sub) a = MkLink sub a
432+ toLink = simpleToLink (Proxy :: Proxy sub)
433+
434+instance HasLink sub => HasLink (IsSecure :> sub) where
435+ type MkLink (IsSecure :> sub) a = MkLink sub a
436+ toLink = simpleToLink (Proxy :: Proxy sub)
437+
438+instance HasLink sub => HasLink (WithNamedContext name context sub) where
439+ type MkLink (WithNamedContext name context sub) a = MkLink sub a
440+ toLink toA _ = toLink toA (Proxy :: Proxy sub)
441+
442+instance HasLink sub => HasLink (RemoteHost :> sub) where
443+ type MkLink (RemoteHost :> sub) a = MkLink sub a
444+ toLink = simpleToLink (Proxy :: Proxy sub)
445+
446+instance HasLink sub => HasLink (BasicAuth realm a :> sub) where
447+ type MkLink (BasicAuth realm a :> sub) r = MkLink sub r
448+ toLink = simpleToLink (Proxy :: Proxy sub)
449+
450+instance HasLink EmptyAPI where
451+ type MkLink EmptyAPI a = EmptyAPI
452+ toLink _ _ _ = EmptyAPI
453+
454+-- Verb (terminal) instances
455+instance HasLink (Verb m s ct a) where
456+ type MkLink (Verb m s ct a) r = r
457+ toLink toA _ = toA
458+
459+instance HasLink Raw where
460+ type MkLink Raw a = a
461+ toLink toA _ = toA
462+
463+instance HasLink (Stream m status fr ct a) where
464+ type MkLink (Stream m status fr ct a) r = r
465+ toLink toA _ = toA
466+
467+-- AuthProtext instances
468+instance HasLink sub => HasLink (AuthProtect tag :> sub) where
469+ type MkLink (AuthProtect tag :> sub) a = MkLink sub a
470+ toLink = simpleToLink (Proxy :: Proxy sub)
471+
472+-- | Helper for implemneting 'toLink' for combinators not affecting link
473+-- structure.
474+simpleToLink
475+ :: forall sub a combinator.
476+ (HasLink sub, MkLink sub a ~ MkLink (combinator :> sub) a)
477+ => Proxy sub
478+ -> (Link -> a)
479+ -> Proxy (combinator :> sub)
480+ -> Link
481+ -> MkLink (combinator :> sub) a
482+simpleToLink _ toA _ = toLink toA (Proxy :: Proxy sub)
483+
484+
485+-- $setup
486+-- >>> import Servant.API
487+-- >>> import Data.Text (Text)
servant/src/Servant/Utils/Links.hs+4−485
@@ -1,487 +1,6 @@
1−{-# LANGUAGE ConstraintKinds #-}
2−{-# LANGUAGE DataKinds #-}
3−{-# LANGUAGE FlexibleContexts #-}
4−{-# LANGUAGE FlexibleInstances #-}
5−{-# LANGUAGE FunctionalDependencies #-}
6−{-# LANGUAGE PolyKinds #-}
7−{-# LANGUAGE ScopedTypeVariables #-}
8−{-# LANGUAGE TypeFamilies #-}
9−{-# LANGUAGE TypeOperators #-}
10−{-# LANGUAGE UndecidableInstances #-}
11−{-# OPTIONS_HADDOCK not-home #-}
12−
13−-- | Type safe generation of internal links.
14−--
15−-- Given an API with a few endpoints:
16−--
17−-- >>> :set -XDataKinds -XTypeFamilies -XTypeOperators
18−-- >>> import Servant.API
19−-- >>> import Servant.Utils.Links
20−-- >>> import Data.Proxy
21−-- >>>
22−-- >>> type Hello = "hello" :> Get '[JSON] Int
23−-- >>> type Bye = "bye" :> QueryParam "name" String :> Delete '[JSON] NoContent
24−-- >>> type API = Hello :<|> Bye
25−-- >>> let api = Proxy :: Proxy API
26−--
27−-- It is possible to generate links that are guaranteed to be within 'API' with
28−-- 'safeLink'. The first argument to 'safeLink' is a type representing the API
29−-- you would like to restrict links to. The second argument is the destination
30−-- endpoint you would like the link to point to, this will need to end with a
31−-- verb like GET or POST. Further arguments may be required depending on the
32−-- type of the endpoint. If everything lines up you will get a 'Link' out the
33−-- other end.
34−--
35−-- You may omit 'QueryParam's and the like should you not want to provide them,
36−-- but types which form part of the URL path like 'Capture' must be included.
37−-- The reason you may want to omit 'QueryParam's is that safeLink is a bit
38−-- magical: if parameters are included that could take input it will return a
39−-- function that accepts that input and generates a link. This is best shown
40−-- with an example. Here, a link is generated with no parameters:
41−--
42−-- >>> let hello = Proxy :: Proxy ("hello" :> Get '[JSON] Int)
43−-- >>> toUrlPiece (safeLink api hello :: Link)
44−-- "hello"
45−--
46−-- If the API has an endpoint with parameters then we can generate links with
47−-- or without those:
48−--
49−-- >>> let with = Proxy :: Proxy ("bye" :> QueryParam "name" String :> Delete '[JSON] NoContent)
50−-- >>> toUrlPiece $ safeLink api with (Just "Hubert")
51−-- "bye?name=Hubert"
52−--
53−-- >>> let without = Proxy :: Proxy ("bye" :> Delete '[JSON] NoContent)
54−-- >>> toUrlPiece $ safeLink api without
55−-- "bye"
56−--
57−-- If you would like create a helper for generating links only within that API,
58−-- you can partially apply safeLink if you specify a correct type signature
59−-- like so:
60−--
61−-- >>> :set -XConstraintKinds
62−-- >>> :{
63−-- >>> let apiLink :: (IsElem endpoint API, HasLink endpoint)
64−-- >>> => Proxy endpoint -> MkLink endpoint Link
65−-- >>> apiLink = safeLink api
66−-- >>> :}
67−--
68−-- `safeLink'` allows to make specialise the output:
69−--
70−-- >>> safeLink' toUrlPiece api without
71−-- "bye"
72−--
73−-- >>> :{
74−-- >>> let apiTextLink :: (IsElem endpoint API, HasLink endpoint)
75−-- >>> => Proxy endpoint -> MkLink endpoint Text
76−-- >>> apiTextLink = safeLink' toUrlPiece api
77−-- >>> :}
78−--
79−-- >>> apiTextLink without
80−-- "bye"
81−--
82−-- Attempting to construct a link to an endpoint that does not exist in api
83−-- will result in a type error like this:
84−--
85−-- >>> let bad_link = Proxy :: Proxy ("hello" :> Delete '[JSON] NoContent)
86−-- >>> safeLink api bad_link
87−-- ...
88−-- ...Could not deduce...
89−-- ...
90−--
91−-- This error is essentially saying that the type family couldn't find
92−-- bad_link under api after trying the open (but empty) type family
93−-- `IsElem'` as a last resort.
94−module Servant.Utils.Links (
95− module Servant.API.TypeLevel,
96−
97− -- * Building and using safe links
98− --
99− -- | Note that 'URI' is from the "Network.URI" module in the @network-uri@ package.
100− safeLink
101− , safeLink'
102− , allLinks
103− , allLinks'
104− , URI(..)
105− -- * Adding custom types
106− , HasLink(..)
107− , Link
108− , linkURI
109− , linkURI'
110− , LinkArrayElementStyle (..)
111− -- ** Link accessors
112− , Param (..)
113− , linkSegments
114− , linkQueryParams
115−) where
116−
117−import Data.List
118−import Data.Proxy
119− (Proxy (..))
120−import Data.Semigroup
121− ((<>))
122−import Data.Singletons.Bool
123− (SBool (..), SBoolI (..))
124−import qualified Data.Text as Text
125−import qualified Data.Text.Encoding as TE
126−import Data.Type.Bool
127− (If)
128−import GHC.TypeLits
129− (KnownSymbol, symbolVal)
130−import Network.URI
131− (URI (..), escapeURIString, isUnreserved)
132−import Prelude ()
133−import Prelude.Compat
134−
135−import Servant.API.Alternative
136− ((:<|>) ((:<|>)))
137−import Servant.API.BasicAuth
138− (BasicAuth)
139−import Servant.API.Capture
140− (Capture', CaptureAll)
141−import Servant.API.Description
142− (Description, Summary)
143−import Servant.API.Empty
144− (EmptyAPI (..))
145−import Servant.API.Experimental.Auth
146− (AuthProtect)
147−import Servant.API.Header
148− (Header')
149−import Servant.API.HttpVersion
150− (HttpVersion)
151−import Servant.API.IsSecure
152− (IsSecure)
153−import Servant.API.Modifiers
154− (FoldRequired)
155−import Servant.API.QueryParam
156− (QueryFlag, QueryParam', QueryParams)
157−import Servant.API.Raw
158− (Raw)
159−import Servant.API.RemoteHost
160− (RemoteHost)
161−import Servant.API.ReqBody
162− (ReqBody')
163−import Servant.API.Stream
164− (Stream)
165−import Servant.API.Sub
166− (type (:>))
167−import Servant.API.TypeLevel
168−import Servant.API.Vault
169− (Vault)
170−import Servant.API.Verbs
171− (Verb)
172−import Servant.API.WithNamedContext
173− (WithNamedContext)
174−import Web.HttpApiData
175−
176−-- | A safe link datatype.
177−-- The only way of constructing a 'Link' is using 'safeLink', which means any
178−-- 'Link' is guaranteed to be part of the mentioned API.
179−data Link = Link
180− { _segments :: [Escaped]
181− , _queryParams :: [Param]
182− } deriving Show
183−
184−newtype Escaped = Escaped String
185−
186−escaped :: String -> Escaped
187−escaped = Escaped . escapeURIString isUnreserved
188−
189−getEscaped :: Escaped -> String
190−getEscaped (Escaped s) = s
191−
192−instance Show Escaped where
193− showsPrec d (Escaped s) = showsPrec d s
194− show (Escaped s) = show s
195−
196−linkSegments :: Link -> [String]
197−linkSegments = map getEscaped . _segments
198−
199−linkQueryParams :: Link -> [Param]
200−linkQueryParams = _queryParams
201−
202−instance ToHttpApiData Link where
203− toHeader = TE.encodeUtf8 . toUrlPiece
204− toUrlPiece l =
205− let uri = linkURI l
206− in Text.pack $ uriPath uri ++ uriQuery uri
207−
208−-- | Query parameter.
209−data Param
210− = SingleParam String Text.Text
211− | ArrayElemParam String Text.Text
212− | FlagParam String
213− deriving Show
214−
215−addSegment :: Escaped -> Link -> Link
216−addSegment seg l = l { _segments = _segments l <> [seg] }
217−
218−addQueryParam :: Param -> Link -> Link
219−addQueryParam qp l =
220− l { _queryParams = _queryParams l <> [qp] }
221−
222−-- | Transform 'Link' into 'URI'.
223−--
224−-- >>> type API = "something" :> Get '[JSON] Int
225−-- >>> linkURI $ safeLink (Proxy :: Proxy API) (Proxy :: Proxy API)
226−-- something
227−--
228−-- >>> type API = "sum" :> QueryParams "x" Int :> Get '[JSON] Int
229−-- >>> linkURI $ safeLink (Proxy :: Proxy API) (Proxy :: Proxy API) [1, 2, 3]
230−-- sum?x[]=1&x[]=2&x[]=3
231−--
232−-- >>> type API = "foo/bar" :> Get '[JSON] Int
233−-- >>> linkURI $ safeLink (Proxy :: Proxy API) (Proxy :: Proxy API)
234−-- foo%2Fbar
235−--
236−-- >>> type SomeRoute = "abc" :> Capture "email" String :> Put '[JSON] ()
237−-- >>> let someRoute = Proxy :: Proxy SomeRoute
238−-- >>> safeLink someRoute someRoute "test@example.com"
239−-- Link {_segments = ["abc","test%40example.com"], _queryParams = []}
240−--
241−-- >>> linkURI $ safeLink someRoute someRoute "test@example.com"
242−-- abc/test%40example.com
243−--
244−linkURI :: Link -> URI
245−linkURI = linkURI' LinkArrayElementBracket
246−
247−-- | How to encode array query elements.
248−data LinkArrayElementStyle
249− = LinkArrayElementBracket -- ^ @foo[]=1&foo[]=2@
250− | LinkArrayElementPlain -- ^ @foo=1&foo=2@
251− deriving (Eq, Ord, Show, Enum, Bounded)
252−
253−-- | Configurable 'linkURI'.
254−--
255−-- >>> type API = "sum" :> QueryParams "x" Int :> Get '[JSON] Int
256−-- >>> linkURI' LinkArrayElementBracket $ safeLink (Proxy :: Proxy API) (Proxy :: Proxy API) [1, 2, 3]
257−-- sum?x[]=1&x[]=2&x[]=3
258−--
259−-- >>> linkURI' LinkArrayElementPlain $ safeLink (Proxy :: Proxy API) (Proxy :: Proxy API) [1, 2, 3]
260−-- sum?x=1&x=2&x=3
261−--
262−linkURI' :: LinkArrayElementStyle -> Link -> URI
263−linkURI' addBrackets (Link segments q_params) =
264− URI mempty -- No scheme (relative)
265− Nothing -- Or authority (relative)
266− (intercalate "/" $ map getEscaped segments)
267− (makeQueries q_params) mempty
1+module Servant.Utils.Links
2+ {-# DEPRECATED "Use Servant.Links." #-}
3+ ( module Servant.Links )
2684 where
269− makeQueries :: [Param] -> String
270− makeQueries [] = ""
271− makeQueries xs =
272− "?" <> intercalate "&" (fmap makeQuery xs)
273−
274− makeQuery :: Param -> String
275− makeQuery (ArrayElemParam k v) = escape k <> style <> escape (Text.unpack v)
276− makeQuery (SingleParam k v) = escape k <> "=" <> escape (Text.unpack v)
277− makeQuery (FlagParam k) = escape k
278−
279− style = case addBrackets of
280− LinkArrayElementBracket -> "[]="
281− LinkArrayElementPlain -> "="
282−
283−escape :: String -> String
284−escape = escapeURIString isUnreserved
285−
286−-- | Create a valid (by construction) relative URI with query params.
287−--
288−-- This function will only typecheck if `endpoint` is part of the API `api`
289−safeLink
290− :: forall endpoint api. (IsElem endpoint api, HasLink endpoint)
291− => Proxy api -- ^ The whole API that this endpoint is a part of
292− -> Proxy endpoint -- ^ The API endpoint you would like to point to
293− -> MkLink endpoint Link
294−safeLink = safeLink' id
295−
296−-- | More general 'safeLink'.
297−--
298−safeLink'
299− :: forall endpoint api a. (IsElem endpoint api, HasLink endpoint)
300− => (Link -> a)
301− -> Proxy api -- ^ The whole API that this endpoint is a part of
302− -> Proxy endpoint -- ^ The API endpoint you would like to point to
303− -> MkLink endpoint a
304−safeLink' toA _ endpoint = toLink toA endpoint (Link mempty mempty)
305−
306−-- | Create all links in an API.
307−--
308−-- Note that the @api@ type must be restricted to the endpoints that have
309−-- valid links to them.
310−--
311−-- >>> type API = "foo" :> Capture "name" Text :> Get '[JSON] Text :<|> "bar" :> Capture "name" Int :> Get '[JSON] Double
312−-- >>> let fooLink :<|> barLink = allLinks (Proxy :: Proxy API)
313−-- >>> :t fooLink
314−-- fooLink :: Text -> Link
315−-- >>> :t barLink
316−-- barLink :: Int -> Link
317−--
318−-- Note: nested APIs don't work well with this approach
319−--
320−-- >>> :kind! MkLink (Capture "nest" Char :> (Capture "x" Int :> Get '[JSON] Int :<|> Capture "y" Double :> Get '[JSON] Double)) Link
321−-- MkLink (Capture "nest" Char :> (Capture "x" Int :> Get '[JSON] Int :<|> Capture "y" Double :> Get '[JSON] Double)) Link :: *
322−-- = Char -> (Int -> Link) :<|> (Double -> Link)
323−allLinks
324− :: forall api. HasLink api
325− => Proxy api
326− -> MkLink api Link
327−allLinks = allLinks' id
328−
329−-- | More general 'allLinks'. See `safeLink'`.
330−allLinks'
331− :: forall api a. HasLink api
332− => (Link -> a)
333− -> Proxy api
334− -> MkLink api a
335−allLinks' toA api = toLink toA api (Link mempty mempty)
336−
337−-- | Construct a toLink for an endpoint.
338−class HasLink endpoint where
339− type MkLink endpoint (a :: *)
340− toLink
341− :: (Link -> a)
342− -> Proxy endpoint -- ^ The API endpoint you would like to point to
343− -> Link
344− -> MkLink endpoint a
345−
346−-- Naked symbol instance
347−instance (KnownSymbol sym, HasLink sub) => HasLink (sym :> sub) where
348− type MkLink (sym :> sub) a = MkLink sub a
349− toLink toA _ =
350− toLink toA (Proxy :: Proxy sub) . addSegment (escaped seg)
351− where
352− seg = symbolVal (Proxy :: Proxy sym)
353−
354−-- QueryParam instances
355−instance (KnownSymbol sym, ToHttpApiData v, HasLink sub, SBoolI (FoldRequired mods))
356− => HasLink (QueryParam' mods sym v :> sub)
357− where
358− type MkLink (QueryParam' mods sym v :> sub) a = If (FoldRequired mods) v (Maybe v) -> MkLink sub a
359− toLink toA _ l mv =
360− toLink toA (Proxy :: Proxy sub) $
361− case sbool :: SBool (FoldRequired mods) of
362− STrue -> (addQueryParam . SingleParam k . toQueryParam) mv l
363− SFalse -> maybe id (addQueryParam . SingleParam k . toQueryParam) mv l
364− where
365− k :: String
366− k = symbolVal (Proxy :: Proxy sym)
367−
368−instance (KnownSymbol sym, ToHttpApiData v, HasLink sub)
369− => HasLink (QueryParams sym v :> sub)
370− where
371− type MkLink (QueryParams sym v :> sub) a = [v] -> MkLink sub a
372− toLink toA _ l =
373− toLink toA (Proxy :: Proxy sub) .
374− foldl' (\l' v -> addQueryParam (ArrayElemParam k (toQueryParam v)) l') l
375− where
376− k = symbolVal (Proxy :: Proxy sym)
377−
378−instance (KnownSymbol sym, HasLink sub)
379− => HasLink (QueryFlag sym :> sub)
380− where
381− type MkLink (QueryFlag sym :> sub) a = Bool -> MkLink sub a
382− toLink toA _ l False =
383− toLink toA (Proxy :: Proxy sub) l
384− toLink toA _ l True =
385− toLink toA (Proxy :: Proxy sub) $ addQueryParam (FlagParam k) l
386− where
387− k = symbolVal (Proxy :: Proxy sym)
388−
389−-- :<|> instance - Generate all links at once
390−instance (HasLink a, HasLink b) => HasLink (a :<|> b) where
391− type MkLink (a :<|> b) r = MkLink a r :<|> MkLink b r
392− toLink toA _ l = toLink toA (Proxy :: Proxy a) l :<|> toLink toA (Proxy :: Proxy b) l
393−
394−-- Misc instances
395−instance HasLink sub => HasLink (ReqBody' mods ct a :> sub) where
396− type MkLink (ReqBody' mods ct a :> sub) r = MkLink sub r
397− toLink toA _ = toLink toA (Proxy :: Proxy sub)
398−
399−instance (ToHttpApiData v, HasLink sub)
400− => HasLink (Capture' mods sym v :> sub)
401− where
402− type MkLink (Capture' mods sym v :> sub) a = v -> MkLink sub a
403− toLink toA _ l v =
404− toLink toA (Proxy :: Proxy sub) $
405− addSegment (escaped . Text.unpack $ toUrlPiece v) l
406−
407−instance (ToHttpApiData v, HasLink sub)
408− => HasLink (CaptureAll sym v :> sub)
409− where
410− type MkLink (CaptureAll sym v :> sub) a = [v] -> MkLink sub a
411− toLink toA _ l vs = toLink toA (Proxy :: Proxy sub) $
412− foldl' (flip $ addSegment . escaped . Text.unpack . toUrlPiece) l vs
413−
414−instance HasLink sub => HasLink (Header' mods sym (a :: *) :> sub) where
415− type MkLink (Header' mods sym a :> sub) r = MkLink sub r
416− toLink = simpleToLink (Proxy :: Proxy sub)
417−
418−instance HasLink sub => HasLink (Vault :> sub) where
419− type MkLink (Vault :> sub) a = MkLink sub a
420− toLink = simpleToLink (Proxy :: Proxy sub)
421−
422−instance HasLink sub => HasLink (Description s :> sub) where
423− type MkLink (Description s :> sub) a = MkLink sub a
424− toLink = simpleToLink (Proxy :: Proxy sub)
425−
426−instance HasLink sub => HasLink (Summary s :> sub) where
427− type MkLink (Summary s :> sub) a = MkLink sub a
428− toLink = simpleToLink (Proxy :: Proxy sub)
429−
430−instance HasLink sub => HasLink (HttpVersion :> sub) where
431− type MkLink (HttpVersion:> sub) a = MkLink sub a
432− toLink = simpleToLink (Proxy :: Proxy sub)
433−
434−instance HasLink sub => HasLink (IsSecure :> sub) where
435− type MkLink (IsSecure :> sub) a = MkLink sub a
436− toLink = simpleToLink (Proxy :: Proxy sub)
437−
438−instance HasLink sub => HasLink (WithNamedContext name context sub) where
439− type MkLink (WithNamedContext name context sub) a = MkLink sub a
440− toLink toA _ = toLink toA (Proxy :: Proxy sub)
441−
442−instance HasLink sub => HasLink (RemoteHost :> sub) where
443− type MkLink (RemoteHost :> sub) a = MkLink sub a
444− toLink = simpleToLink (Proxy :: Proxy sub)
445−
446−instance HasLink sub => HasLink (BasicAuth realm a :> sub) where
447− type MkLink (BasicAuth realm a :> sub) r = MkLink sub r
448− toLink = simpleToLink (Proxy :: Proxy sub)
449−
450−instance HasLink EmptyAPI where
451− type MkLink EmptyAPI a = EmptyAPI
452− toLink _ _ _ = EmptyAPI
453−
454−-- Verb (terminal) instances
455−instance HasLink (Verb m s ct a) where
456− type MkLink (Verb m s ct a) r = r
457− toLink toA _ = toA
458−
459−instance HasLink Raw where
460− type MkLink Raw a = a
461− toLink toA _ = toA
462−
463−instance HasLink (Stream m fr ct a) where
464− type MkLink (Stream m fr ct a) r = r
465− toLink toA _ = toA
466−
467−-- AuthProtext instances
468−instance HasLink sub => HasLink (AuthProtect tag :> sub) where
469− type MkLink (AuthProtect tag :> sub) a = MkLink sub a
470− toLink = simpleToLink (Proxy :: Proxy sub)
471−
472−-- | Helper for implemneting 'toLink' for combinators not affecting link
473−-- structure.
474−simpleToLink
475− :: forall sub a combinator.
476− (HasLink sub, MkLink sub a ~ MkLink (combinator :> sub) a)
477− => Proxy sub
478− -> (Link -> a)
479− -> Proxy (combinator :> sub)
480− -> Link
481− -> MkLink (combinator :> sub) a
482−simpleToLink _ toA _ = toLink toA (Proxy :: Proxy sub)
483−
4845
485−-- $setup
486−-- >>> import Servant.API
487−-- >>> import Data.Text (Text)
6+import Servant.Links
servant/test/Servant/Utils/LinksSpec.hs → servant/test/Servant/LinksSpec.hsrenamed+3−3
@@ -7,7 +7,7 @@
77#if __GLASGOW_HASKELL__ < 709
88{-# OPTIONS_GHC -fcontext-stack=41 #-}
99#endif
10−module Servant.Utils.LinksSpec where
10+module Servant.LinksSpec where
1111
1212import Data.Proxy (Proxy (..))
1313import Test.Hspec (Expectation, Spec, describe, it,
@@ -15,7 +15,7 @@ import Test.Hspec (Expectation, Spec, describe, it,
1515import Data.String (fromString)
1616
1717import Servant.API
18−import Servant.Utils.Links
18+import Servant.Links
1919import Servant.API.Internal.Test.ComprehensiveAPI (comprehensiveAPIWithoutRaw)
2020
2121type TestApi =
@@ -51,7 +51,7 @@ shouldBeLink link expected =
5151 toUrlPiece link `shouldBe` fromString expected
5252
5353spec :: Spec
54−spec = describe "Servant.Utils.Links" $ do
54+spec = describe "Servant.Links" $ do
5555 it "generates correct links for capture query params" $ do
5656 let l1 = Proxy :: Proxy ("hello" :> Capture "name" String :> Delete '[JSON] NoContent)
5757 apiLink l1 "hi" `shouldBeLink` "hello/hi"
stack.yaml+1−14
@@ -1,5 +1,5 @@
11# Let's try to keep resolver at the first day of the month
2−resolver: nightly-2018-03-01
2+resolver: nightly-2018-06-01
33packages:
44- servant-client/
55- servant-client-core/
@@ -8,18 +8,5 @@ packages:
88- servant-server/
99- servant/
1010
11−extra-deps:
12−- cabal-doctest-1.0.6
13−- http-api-data-0.3.7.2
14−- http-types-0.12
15−- text-1.2.3.0
16−- aeson-1.3.0.0
17−- exceptions-0.10.0
18−- aeson-compat-0.3.7.1
19−- free-5.0.1
20−- lens-4.16
21−- random-bytestring-0.1.3
22−- pcg-random-0.1.3.5
23−
2411# allow-newer: true # ignores all bounds, that's a sledgehammer
2512# - doc/tutorial/