git.iko.soy/typeable/servant
on master help sign in
curl-mock cookbook example
commit765b62b05bc4391379c0e28837b5bea535d8610e authorJakob Demler <jdemler@curry-software.com> dateFri 14 Sep 2018, 19:01 (8 years ago) parente066fbe
6 files changed+258−6
.travis.yml+10−6 cabal.project+1 doc/cookbook/cabal.project+1 doc/cookbook/curl-mock/CurlMock.lhs+218 doc/cookbook/curl-mock/curl-mock.cabal+27 doc/cookbook/index.rst+1
.travis.yml+10−6
@@ -66,11 +66,11 @@ install:
6666 - rm -fv cabal.project cabal.project.local
6767 - "if [ $HCNUMVER -ge 70800 ]; then sed -i.bak 's/-- ghc-options:.*/ghc-options: -j2/' ${HOME}/.cabal/config; fi"
6868 - grep -Ev -- '^\s*--' ${HOME}/.cabal/config | grep -Ev '^\s*$'
69− - "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/generic\" \"doc/cookbook/https\" \"doc/cookbook/jwt-and-basic-auth\" \"doc/cookbook/pagination\" \"doc/cookbook/structuring-apis\" \"doc/cookbook/using-custom-monad\" \"doc/cookbook/using-free-client\"\\n' > cabal.project"
69+ - "printf 'packages: \"servant\" \"servant-client\" \"servant-client-core\" \"servant-docs\" \"servant-foreign\" \"servant-server\" \"doc/tutorial\" \"doc/cookbook/basic-auth\" \"doc/cookbook/curl-mock\" \"doc/cookbook/db-postgres-pool\" \"doc/cookbook/db-sqlite-simple\" \"doc/cookbook/file-upload\" \"doc/cookbook/generic\" \"doc/cookbook/https\" \"doc/cookbook/jwt-and-basic-auth\" \"doc/cookbook/pagination\" \"doc/cookbook/structuring-apis\" \"doc/cookbook/using-custom-monad\" \"doc/cookbook/using-free-client\"\\n' > cabal.project"
7070 - "echo 'constraints: foundation >=0.0.14,memory <0.14.12 || >0.14.12' >> cabal.project"
7171 - "echo 'allow-newer: servant-auth-server:http-types,servant-auth-server:servant-server, servant-pagination:servant,servant-pagination:servant-server' >> cabal.project"
7272 - touch cabal.project.local
73− - "if ! $NOINSTALLEDCONSTRAINTS; then for pkg in $($HCPKG list --simple-output); do echo $pkg | grep -vw -- servant | grep -vw -- servant-client | grep -vw -- servant-client-core | grep -vw -- servant-docs | grep -vw -- servant-foreign | grep -vw -- servant-server | grep -vw -- tutorial | grep -vw -- cookbook-basic-auth | grep -vw -- cookbook-db-postgres-pool | grep -vw -- cookbook-db-sqlite-simple | grep -vw -- cookbook-file-upload | grep -vw -- cookbook-generic | grep -vw -- cookbook-https | grep -vw -- cookbook-jwt-and-basic-auth | grep -vw -- cookbook-pagination | grep -vw -- cookbook-structuring-apis | grep -vw -- cookbook-using-custom-monad | grep -vw -- cookbook-using-free-client | sed 's/^/constraints: /' | sed 's/-[^-]*$/ installed/' >> cabal.project.local; done; fi"
73+ - "if ! $NOINSTALLEDCONSTRAINTS; then for pkg in $($HCPKG list --simple-output); do echo $pkg | grep -vw -- servant | grep -vw -- servant-client | grep -vw -- servant-client-core | grep -vw -- servant-docs | grep -vw -- servant-foreign | grep -vw -- servant-server | grep -vw -- tutorial | grep -vw -- cookbook-basic-auth | grep -vw -- cookbook-curl-mock | grep -vw -- cookbook-db-postgres-pool | grep -vw -- cookbook-db-sqlite-simple | grep -vw -- cookbook-file-upload | grep -vw -- cookbook-generic | grep -vw -- cookbook-https | grep -vw -- cookbook-jwt-and-basic-auth | grep -vw -- cookbook-pagination | grep -vw -- cookbook-structuring-apis | grep -vw -- cookbook-using-custom-monad | grep -vw -- cookbook-using-free-client | sed 's/^/constraints: /' | sed 's/-[^-]*$/ installed/' >> cabal.project.local; done; fi"
7474 - cat cabal.project || true
7575 - cat cabal.project.local || true
7676 - if [ -f "servant/configure.ac" ]; then
@@ -97,6 +97,9 @@ install:
9797 - if [ -f "doc/cookbook/basic-auth/configure.ac" ]; then
9898 (cd "doc/cookbook/basic-auth" && autoreconf -i);
9999 fi
100+ - if [ -f "doc/cookbook/curl-mock/configure.ac" ]; then
101+ (cd "doc/cookbook/curl-mock" && autoreconf -i);
102+ fi
100103 - if [ -f "doc/cookbook/db-postgres-pool/configure.ac" ]; then
101104 (cd "doc/cookbook/db-postgres-pool" && autoreconf -i);
102105 fi
@@ -128,7 +131,7 @@ install:
128131 (cd "doc/cookbook/using-free-client" && autoreconf -i);
129132 fi
130133 - rm -f cabal.project.freeze
131− - 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/generic"/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 "doc/cookbook/using-free-client"/dist
134+ - 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/curl-mock"/dist "doc/cookbook/basic-auth"/dist "doc/cookbook/curl-mock"/dist "doc/cookbook/db-postgres-pool"/dist "doc/cookbook/db-sqlite-simple"/dist "doc/cookbook/file-upload"/dist "doc/cookbook/generic"/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 "doc/cookbook/using-free-client"/dist
132135 - DISTDIR=$(mktemp -d /tmp/dist-test.XXXX)
133136
134137# Here starts the actual work to be performed for the package under test;
@@ -144,6 +147,7 @@ script:
144147 - (cd "servant-server" && cabal sdist)
145148 - (cd "doc/tutorial" && cabal sdist)
146149 - (cd "doc/cookbook/basic-auth" && cabal sdist)
150+ - (cd "doc/cookbook/curl-mock" && cabal sdist)
147151 - (cd "doc/cookbook/db-postgres-pool" && cabal sdist)
148152 - (cd "doc/cookbook/db-sqlite-simple" && cabal sdist)
149153 - (cd "doc/cookbook/file-upload" && cabal sdist)
@@ -156,14 +160,14 @@ script:
156160 - (cd "doc/cookbook/using-free-client" && cabal sdist)
157161 - echo -en 'travis_fold:end:sdist\\r'
158162 - echo Unpacking... && echo -en 'travis_fold:start:unpack\\r'
159− - 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/generic"/dist/cookbook-generic-*.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 "doc/cookbook/using-free-client"/dist/cookbook-using-free-client-*.tar.gz ${DISTDIR}/
163+ - 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/curl-mock"/dist/cookbook-curl-mock-*.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/generic"/dist/cookbook-generic-*.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 "doc/cookbook/using-free-client"/dist/cookbook-using-free-client-*.tar.gz ${DISTDIR}/
160164 - cd ${DISTDIR} || false
161165 - find . -maxdepth 1 -name '*.tar.gz' -exec tar -xvf '{}' \;
162− - "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-generic-*/*.cabal cookbook-https-*/*.cabal cookbook-jwt-and-basic-auth-*/*.cabal cookbook-pagination-*/*.cabal cookbook-structuring-apis-*/*.cabal cookbook-using-custom-monad-*/*.cabal cookbook-using-free-client-*/*.cabal\\n' > cabal.project"
166+ - "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-curl-mock-*/*.cabal cookbook-db-postgres-pool-*/*.cabal cookbook-db-sqlite-simple-*/*.cabal cookbook-file-upload-*/*.cabal cookbook-generic-*/*.cabal cookbook-https-*/*.cabal cookbook-jwt-and-basic-auth-*/*.cabal cookbook-pagination-*/*.cabal cookbook-structuring-apis-*/*.cabal cookbook-using-custom-monad-*/*.cabal cookbook-using-free-client-*/*.cabal\\n' > cabal.project"
163167 - "echo 'constraints: foundation >=0.0.14,memory <0.14.12 || >0.14.12' >> cabal.project"
164168 - "echo 'allow-newer: servant-auth-server:http-types,servant-auth-server:servant-server, servant-pagination:servant,servant-pagination:servant-server' >> cabal.project"
165169 - touch cabal.project.local
166− - "if ! $NOINSTALLEDCONSTRAINTS; then for pkg in $($HCPKG list --simple-output); do echo $pkg | grep -vw -- servant | grep -vw -- servant-client | grep -vw -- servant-client-core | grep -vw -- servant-docs | grep -vw -- servant-foreign | grep -vw -- servant-server | grep -vw -- tutorial | grep -vw -- cookbook-basic-auth | grep -vw -- cookbook-db-postgres-pool | grep -vw -- cookbook-db-sqlite-simple | grep -vw -- cookbook-file-upload | grep -vw -- cookbook-generic | grep -vw -- cookbook-https | grep -vw -- cookbook-jwt-and-basic-auth | grep -vw -- cookbook-pagination | grep -vw -- cookbook-structuring-apis | grep -vw -- cookbook-using-custom-monad | grep -vw -- cookbook-using-free-client | sed 's/^/constraints: /' | sed 's/-[^-]*$/ installed/' >> cabal.project.local; done; fi"
170+ - "if ! $NOINSTALLEDCONSTRAINTS; then for pkg in $($HCPKG list --simple-output); do echo $pkg | grep -vw -- servant | grep -vw -- servant-client | grep -vw -- servant-client-core | grep -vw -- servant-docs | grep -vw -- servant-foreign | grep -vw -- servant-server | grep -vw -- tutorial | grep -vw -- cookbook-basic-auth | grep -vw -- cookbook-curl-mock | grep -vw -- cookbook-db-postgres-pool | grep -vw -- cookbook-db-sqlite-simple | grep -vw -- cookbook-file-upload | grep -vw -- cookbook-generic | grep -vw -- cookbook-https | grep -vw -- cookbook-jwt-and-basic-auth | grep -vw -- cookbook-pagination | grep -vw -- cookbook-structuring-apis | grep -vw -- cookbook-using-custom-monad | grep -vw -- cookbook-using-free-client | sed 's/^/constraints: /' | sed 's/-[^-]*$/ installed/' >> cabal.project.local; done; fi"
167171 - cat cabal.project || true
168172 - cat cabal.project.local || true
169173 - echo -en 'travis_fold:end:unpack\\r'
cabal.project+1
@@ -9,6 +9,7 @@ packages: servant/
99 -- doc/cookbook/*/*.cabal
1010
1111 doc/cookbook/basic-auth
12+ doc/cookbook/curl-mock
1213 doc/cookbook/db-postgres-pool
1314 doc/cookbook/db-sqlite-simple
1415 doc/cookbook/file-upload
doc/cookbook/cabal.project+1
@@ -1,5 +1,6 @@
11packages:
22 basic-auth/
3+ curl-mock/
34 db-sqlite-simple/
45 db-postgres-pool/
56 using-custom-monad/
doc/cookbook/curl-mock/CurlMock.lhsnew file+218
@@ -0,0 +1,218 @@
1+# Generating mock curl calls
2+
3+In this example we will generate curl requests with mock post data from a servant API.
4+This may be usefull for testing and development purposes.
5+Especially post requests with a request body are tedious to send manually.
6+
7+Also, we will learn how to use the servant-forein library to generate stuff from servant APIs.
8+
9+
10+Language extensions and imports:
11+``` haskell
12+{-# LANGUAGE DataKinds #-}
13+{-# LANGUAGE DeriveGeneric #-}
14+{-# LANGUAGE FlexibleContexts #-}
15+{-# LANGUAGE FlexibleInstances #-}
16+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
17+{-# LANGUAGE MultiParamTypeClasses #-}
18+{-# LANGUAGE OverloadedStrings #-}
19+{-# LANGUAGE RankNTypes #-}
20+{-# LANGUAGE RecordWildCards #-}
21+{-# LANGUAGE ScopedTypeVariables #-}
22+{-# LANGUAGE TypeOperators #-}
23+
24+import Control.Lens ((^.))
25+import Data.Aeson.Compat
26+import Data.Aeson.Text
27+import Data.Monoid ((<>))
28+import Data.Proxy (Proxy (Proxy))
29+import Data.Text (Text)
30+import Data.Text.Encoding (decodeUtf8)
31+import qualified Data.Text.IO as T.IO
32+import qualified Data.Text.Lazy as LazyT
33+import GHC.Generics
34+import Servant ((:<|>), (:>), Get, JSON,
35+ Post, ReqBody)
36+import Servant.Foreign (Foreign, GenerateList,
37+ HasForeign, HasForeignType, Req,
38+ Segment, SegmentType (Cap, Static),
39+ argName, listFromAPI, path,
40+ reqBody, reqMethod, reqUrl, typeFor,
41+ unPathSegment, unSegment,)
42+import Test.QuickCheck.Arbitrary
43+import Test.QuickCheck.Arbitrary.Generic
44+import Test.QuickCheck.Gen (generate)
45+import qualified Data.Text as T
46+
47+```
48+
49+
50+Let's define our API:
51+
52+``` haskell
53+type UserAPI = "users" :> Get '[JSON] [User]
54+ :<|> "new" :> "user" :> ReqBody '[JSON] User :> Post '[JSON] ()
55+
56+data User = User
57+ { name :: String
58+ , age :: Int
59+ , email :: String
60+ } deriving (Eq, Show, Generic)
61+
62+instance Arbitrary User where
63+ arbitrary = genericArbitrary
64+ shrink = genericShrink
65+instance ToJSON User
66+instance FromJSON User
67+```
68+
69+Notice the `Arbitrary User` instance which we will later need to create mock data.
70+
71+Also, the obligatory servant boilerplate:
72+
73+``` haskell
74+api :: Proxy UserAPI
75+api = Proxy
76+```
77+
78+
79+## servant-forgein and the HasForeignType Class
80+
81+Servant-foreign allows us to look into the API we designed.
82+The entry point is `listFromAPI` which takes three types and returns a list of endpoints:
83+
84+``` haskell ignore
85+listFromAPI :: (HasForeign lang ftype api, GenerateList ftype (Foreign ftype api)) => Proxy lang -> Proxy ftype -> Proxy api -> [Req ftype]
86+```
87+
88+This looks a bit confusing...
89+[Here](https://hackage.haskell.org/package/servant-foreign-0.11.1/docs/Servant-Foreign.html#t:HasForeignType) is the documentation for the `HasForeign` typeclass.
90+We will not go into details here, but this allows us to create a value of type `ftype` for any type `a` in our API.
91+
92+In our case we want to create a mock of every type `a`.
93+
94+We create a new datatype that holds our mocked value. Well, not the mocked value itself. To mock it we need IO (random). So the promise of a mocked value after some IO is performed:
95+
96+``` haskell
97+data NoLang
98+
99+data Mocked = Mocked (IO Text)
100+```
101+
102+Now, we create an instance of `HasForeignType` for `NoLang` and `Mocked` for every `a` that implements `ToJSON` and `Arbitrary`:
103+``` haskell
104+instance (ToJSON a, Arbitrary a) => HasForeignType NoLang Mocked a where
105+ typeFor _ _ _ =
106+ Mocked (genText (Proxy :: Proxy a))
107+```
108+
109+What does `genText` do? It generates an arbitrary value of type `a` and encodes it as text. (And does some lazy to non-lazy text transformation we do not care about):
110+
111+``` haskell
112+genText :: (ToJSON a, Arbitrary a) => Proxy a -> IO Text
113+genText p =
114+ fmap (\v -> LazyT.toStrict $ encodeToLazyText v) (genArb p)
115+
116+genArb :: Arbitrary a => Proxy a -> IO a
117+genArb _ =
118+ generate arbitrary
119+```
120+
121+### Generating curl calls for every endpoint
122+
123+Everything is prepared now and we can start generating some curl calls.
124+
125+``` haskell
126+generateCurl :: (GenerateList Mocked (Foreign Mocked api), HasForeign NoLang Mocked api)
127+ => Proxy api
128+ -> Text
129+ -> IO Text
130+generateCurl p host =
131+ fmap T.unlines body
132+ where
133+ body = foldr (\endp curlCalls -> mCons (generateEndpoint host endp) curlCalls) (return [])
134+ $ listFromAPI (Proxy :: Proxy NoLang) (Proxy :: Proxy Mocked) p
135+```
136+
137+To understand the, better start at the end:
138+
139+`listFromAPI` gives us a list of endpoints. We iterate over them (`foldr`) and call `generateEndpoint` for every endpoint.
140+
141+As generate endpoint will not return `Text` but `IO Text` (remember we need some random bits to mock), we cannot just use the cons operator but need to build `IO [Text]` from `IO Text`s.
142+
143+``` haskell
144+mCons :: IO a -> IO [a] -> IO [a]
145+mCons ele list =
146+ ele >>= \e -> list >>= \l -> return ( e : l )
147+```
148+
149+
150+Now comes the juicy part; accessing the endpoints data:
151+
152+``` haskell
153+generateEndpoint :: Text -> Req Mocked -> IO Text
154+generateEndpoint host req =
155+ case maybeBody of
156+ Just body ->
157+ body >>= \b -> return $ T.intercalate " " [ "curl", "-X", method, "-d", "'" <> b <> "'"
158+ , "-H 'Content-Type: application/json'", host <> "/" <> url ]
159+ Nothing ->
160+ return $ T.intercalate " " [ "curl", "-X", method, host <> "/" <> url ]
161+ where
162+ method = decodeUtf8 $ req ^. reqMethod
163+
164+ url = T.intercalate "/" $ map segment (req ^. reqUrl . path)
165+
166+ maybeBody = fmap (\(Mocked io) -> io) (req ^. reqBody)
167+
168+```
169+`servant-foreign` offers a multitude of lenses to be used with `Req`-values.
170+
171+`reqMethod` gives us a straigthforward `Network.HTTP.Types.Method`, `reqUrl` the url part and so on.
172+Just take a look at [the docs](https://hackage.haskell.org/package/servant-foreign-0.11.1/docs/Servant-Foreign.html).
173+
174+But how do we get our mocked json string? This seems to be a bit to short to be true:
175+
176+``` haskell ignore
177+maybeBody = fmap (\(Mocked io) -> io) (req ^. reqBody)
178+```
179+
180+But it is that simple!
181+The docs say `reqBody` gives us a `Maybe f`. What is `f`, you ask? As defined in `generateCurl`, `f` is `Mocked` and contains a `IO Text`. How is this `Mocked` value created? The `HasForeignType::typeFor` does it!
182+
183+Of course only if the endpoint has a request body.
184+
185+
186+Some (incomplete) code for url segments:
187+``` haskell
188+segment :: Segment Mocked -> Text
189+segment seg =
190+ case unSegment seg of
191+ Static p ->
192+ unPathSegment p
193+
194+ Cap arg ->
195+ -- Left as exercise for the reader: Mock args in the url
196+ unPathSegment $ arg ^. argName
197+```
198+
199+And now, lets hook it all up in our main function:
200+
201+``` haskell
202+main :: IO ()
203+main =
204+ generateCurl api "localhost:8081" >>= (\v -> T.IO.putStrLn v)
205+```
206+
207+Done:
208+
209+``` curl
210+curl -X GET localhost:8081/users
211+curl -X POST -d '{"email":"wV򝣀_b򆎘:z񁊞򲙲!(3DM V","age":10,"name":"=|W"}' -H 'Content-Type: application/json' localhost:8081/new/user
212+
213+```
214+
215+This is of course no complete curl call mock generator, many things including path arguments are missing.
216+But it correctly generate mock calls for simple POST requests.
217+
218+Also, we now know how to use `HasForeignType` and `listFromAPI` to generate anything we want.
doc/cookbook/curl-mock/curl-mock.cabalnew file+27
@@ -0,0 +1,27 @@
1+name: cookbook-curl-mock
2+version: 0.1
3+synopsis: Generate curl mock requests cookbook example
4+homepage: http://haskell-servant.readthedocs.org
5+license: BSD3
6+license-file: ../../../servant/LICENSE
7+author: Servant Contributors
8+maintainer: haskell-servant-maintainers@googlegroups.com
9+build-type: Simple
10+cabal-version: >=1.10
11+tested-with: GHC==8.0.2, GHC==8.2.2, GHC==8.4.3
12+
13+executable cookbock-curl-mock
14+ main-is: CurlMock.lhs
15+ build-depends: base == 4.*
16+ , aeson
17+ , aeson-compat
18+ , lens
19+ , text
20+ , servant
21+ , servant-server
22+ , servant-foreign
23+ , QuickCheck
24+ , generic-arbitrary
25+ default-language: Haskell2010
26+ ghc-options: -Wall -pgmL markdown-unlit
27+ build-tool-depends: markdown-unlit:markdown-unlit
doc/cookbook/index.rst+1
@@ -28,3 +28,4 @@ you name it!
2828 jwt-and-basic-auth/JWTAndBasicAuth.lhs
2929 file-upload/FileUpload.lhs
3030 pagination/Pagination.lhs
31+ curl-mock/CurlMock.lhs