git.iko.soy/typeable/servant
on master help sign in
Revert "introducing NamedRoutes cookbook"

This reverts commit 5c80214351d7ef376de3616e23fc4e09353790fb.

commitf4cd56446ba29a815783862befaab2eec94f266e authorakhesaCaro <caroline.gaudreau@gmail.com> dateFri 18 Feb 2022, 12:13 (5 years ago) parent50355d0
3 files changed−401
cabal.project−1 doc/cookbook/namedRoutes/NamedRoutes.lhs−373 doc/cookbook/namedRoutes/namedRoutes.cabal−27
cabal.project−1
@@ -46,7 +46,6 @@ packages:
4646 doc/cookbook/using-custom-monad
4747 doc/cookbook/using-free-client
4848 -- doc/cookbook/open-id-connect
49− doc/cookbook/namedRoutes
5049
5150tests: True
5251optimization: False
doc/cookbook/namedRoutes/NamedRoutes.lhsdeleted−373
@@ -1,373 +0,0 @@
1−# NamedRoutes - Using records to define APIs
2−
3−*Available in Servant 0.19 or higher*
4−
5−Servant offers a very natural way of constructing APIs with nested records, called `NamedRoutes`.
6−
7−This cookbook explains how to implement such nested-record-APIs using `NamedRoutes` through the example of a Movie Catalog.
8−
9−First, we start by constructing the domain types of our Movie Catalog.
10−After, we show you how to implement the API type with the NamedRoutes records.
11−Lastly, we make a Server and a Client out of the API type.
12−
13−However, it should be understood that this cookbook does _not_ dwell on the built-in servant combinators as the [<Structuring APIs> cookbook ](<https://docs.servant.dev/en/stable/cookbook/structuring-apis/StructuringApis.html>) already covers that angle.
14−
15−## Why would I want to use `NamedRoutes` over the alternative `:<|>` operator?
16−
17−With `NamedRoutes`, we don’t need to care about the declaration order of the endpoints.
18−For example, with the `:<|>` operator there’s room for error when the order of the API type
19−
20−```haskell,ignore
21−type API1 = "version" :> Get '[JSON] Version
22− :<|> "movies" :> Get '[JSON] [Movie]
23−```
24−
25−does not follow the `Handler` implementation order
26−
27−```haskell,ignore
28−apiHandler :: ServerT API1 Handler
29−apiHandler = getMovies
30− :<|> getVersion
31−```
32−
33−GHC could scold you with a very tedious message such as :
34−
35−```console
36− • Couldn't match type 'Handler NoContent'
37− with 'Movie -> Handler NoContent'
38− Expected type: ServerT MovieCatalogAPI Handler
39− Actual type: Handler Version
40− :<|> ((Maybe SortBy -> Handler [Movie])
41− :<|> ((MovieId -> Handler (Maybe Movie))
42− :<|> ((MovieId -> Movie -> Handler NoContent)
43− :<|> (MovieId -> Handler NoContent))))
44− • In the expression:
45− versionHandler
46− :<|>
47− movieListHandler
48− :<|>
49− getMovieHandler :<|> updateMovieHandler :<|> deleteMovieHandler
50− In an equation for 'server':
51− server
52− = versionHandler
53− :<|>
54− movieListHandler
55− :<|>
56− getMovieHandler :<|> updateMovieHandler :<|> deleteMovieHandler
57− |
58−226 | server = versionHandler
59−```
60−
61−On the contrary, with the `NamedRoutes` technique, we refer to the routes by their name:
62−
63−```haskell,ignore
64−data API mode = API
65− { list :: "list" :> ...
66− , delete :: "delete" :> ...
67− }
68−```
69−
70−and GHC follows the lead :
71−
72−```console
73− • Couldn't match type 'NoContent' with 'Movie'
74− Expected type: AsServerT Handler :- Delete '[JSON] Movie
75− Actual type: Handler NoContent
76− • In the 'delete' field of a record
77− In the expression:
78− MovieAPI
79− {get = getMovieHandler movieId,
80− update = updateMovieHandler movieId,
81− delete = deleteMovieHandler movieId}
82− In an equation for 'movieHandler':
83− movieHandler movieId
84− = MovieAPI
85− {get = getMovieHandler movieId,
86− update = updateMovieHandler movieId,
87− delete = deleteMovieHandler movieId}
88− |
89−252 | , delete = deleteMovieHandler movieId
90−```
91−
92−So, NamedRoutes is more readable for a human, and GHC gives you more accurate error messages.
93−
94−What are we waiting for?
95−
96−
97−## Boilerplate time!
98−
99−First, let’s get rid of the the extensions and imports boilerplate in order to focus on our new technique:
100−
101−
102−```haskell
103−{-# LANGUAGE DataKinds #-}
104−{-# LANGUAGE DeriveAnyClass #-}
105−{-# LANGUAGE DerivingStrategies #-}
106−{-# LANGUAGE DeriveGeneric #-}
107−{-# LANGUAGE OverloadedStrings #-}
108−{-# LANGUAGE TypeOperators #-}
109−
110−import GHC.Generics ( Generic )
111−import Data.Aeson ( FromJSON, ToJSON )
112−import Data.Proxy ( Proxy(..) )
113−import Network.Wai.Handler.Warp ( run )
114−
115−import Servant ( NamedRoutes
116− , Handler, serve )
117−import Servant.API (Capture, Delete, Get, Put, QueryParam, ReqBody
118− , JSON, NoContent (..)
119− , FromHttpApiData (..),ToHttpApiData(..)
120− , (:>) )
121−import Servant.API.Generic ( (:-) )
122−
123−import Servant.Client ( AsClientT, ClientM, client
124− , (//), (/:) )
125−import Servant.Client.Generic ()
126−
127−import Servant.Server ( Application, ServerT )
128−import Servant.Server.Generic ( AsServerT )
129−
130−```
131−
132−## Domain context
133−
134−Now that we’ve handled the boilerplate, we can dive into our Movie Catalog domain.
135−
136−Consider a `Movie` constructed from a `Title` and a `Year` of publication.
137−
138−``` haskell
139−data Movie = Movie
140− { movieId :: MovieId
141− , title :: Title
142− , year :: Year
143− }
144− deriving stock Generic
145− deriving anyclass (FromJSON, ToJSON)
146−
147−type MovieId = String
148−type Title = String
149−type Year = Int
150−
151−```
152−
153−
154−Let’s forget about the deriving stuff for now and think about the API that we want to make.
155−
156−```
157− "version" -> Get Version
158− /
159−api "list" -> Get [Movie] ?sortBy= Title | Year (sort by the Title or the Year)
160− \ /
161− "movies" Get Movie
162− \ /
163− Capture MovieId - Put Movie
164− \
165− Delete MovieId
166−```
167−
168−In this example, we create a very simple endpoint for the Version,
169−and several complex endpoints that use nested records for the CRUD part of the movie.
170−
171−So, the URLs would look like
172−
173−- GET …/version
174−- GET …/movies/list?sortby=Title
175−- GET …/movies/<MovieId>/
176−- PUT …/movies/<MovieId>/
177−- DELETE …/movies/<MovieId>
178−
179−### API Type
180−
181−Now that we have a very clear idea of the API we want to make, we need to transform it into usable Haskell code:
182−
183−``` haskell
184−
185−data API mode = API
186− { version :: mode :- "version" :> Get '[JSON] Version
187− , movies :: mode :- "movies" :> NamedRoutes MoviesAPI
188− } deriving stock Generic
189−
190−type Version = String -- This will do for the sake of example.
191−
192−```
193−Here, we see the first node of our tree. It contains the two branches “version” and “movies” respectively:
194−
195−The “version” branch is very simple and self-explanatory.
196−The “movies” branch will contain another node, represented by another record (see above). That is why we need the `NameRoutes` helper.
197−
198−Note:
199−
200−The `mode` type parameter indicates into which implementation the record’s `Generic` representation will be transformed—as a client or as a server. We will discuss that later.
201−
202−Let's jump into the "movies" subtree node:
203−
204−
205−``` haskell
206−
207−data MoviesAPI mode = MoviesAPI
208− { list :: mode :- "list" :> QueryParam "SortBy" SortBy :> Get '[JSON] [Movie]
209− , movie :: mode :- Capture "movieId" MovieId :> NamedRoutes MovieAPI
210− } deriving stock Generic
211−
212−data SortBy = Year | Title
213−
214−instance ToHttpApiData SortBy where
215− toQueryParam Year = "year"
216− toQueryParam Title = "title"
217−
218−instance FromHttpApiData SortBy where
219− parseQueryParam "year" = Right Year
220− parseQueryParam "title" = Right Title
221− parseQueryParam param = Left $ param <> " is not a valid value"
222−
223−```
224−So, remember, this type represents the `MoviesAPI` node that we’ve connected earlier to the main `API` tree.
225−
226−In this subtree, we illustrated both an endpoint with a **query param** and also, a **capture** with a subtree underneath it.
227−
228−So, let's go deeper into our API tree.
229−
230−``` haskell
231−data MovieAPI mode = MovieAPI
232− { get :: mode :- Get '[JSON] (Maybe Movie)
233− , update :: mode :- ReqBody '[JSON] Movie :> Put '[JSON] NoContent
234− , delete :: mode :- Delete '[JSON] NoContent
235− } deriving stock Generic
236−```
237−
238−As you can see, we end up implementing the deepest routes of our API.
239−
240−Small detail: as our main API tree is also a record, we need the `NamedRoutes` helper.
241−To improve readability, we suggest you create a type alias:
242−
243−``` haskell
244−type MovieCatalogAPI = NamedRoutes API
245−```
246−
247−That's it, we have our `MovieCatalogAPI` type!
248−
249−Let's make a server and a client out of it!
250−
251−### The Server
252−
253−As you know, we can’t talk about a server, without addressing the handlers.
254−
255−First, we take our handlers…
256−
257−```haskell
258−versionHandler :: Handler Version
259−versionHandler = pure "0.0.1"
260−
261−movieListHandler :: Maybe SortBy -> Handler [Movie]
262−movieListHandler _ = pure moviesDB
263−
264−moviesDB :: [Movie]
265−moviesDB =
266− [ Movie "1" "Se7en" 1995
267− , Movie "2" "Minority Report" 2002
268− , Movie "3" "The Godfather" 1972
269− ]
270−
271−getMovieHandler :: MovieId -> Handler (Maybe Movie)
272−getMovieHandler requestMovieId = go moviesDB
273− where
274− go [] = pure Nothing
275− go (movie:ms) | movieId movie == requestMovieId = pure $ Just movie
276− go (m:ms) = go ms
277−
278−updateMovieHandler :: MovieId -> Movie -> Handler NoContent
279−updateMovieHandler requestedMovieId newMovie =
280− -- update the movie list in the database...
281− pure NoContent
282−
283−deleteMovieHandler :: MovieId -> Handler NoContent
284−deleteMovieHandler _ =
285− -- delete the movie from the database...
286− pure NoContent
287−
288−```
289−
290−And assemble them together with the record structure, which is the glue here.
291−
292−```haskell
293−server :: ServerT MovieCatalogAPI Handler
294−server =
295− API
296− { version = versionHandler
297− , movies = moviesHandler
298− }
299−
300−moviesHandler :: MoviesAPI (AsServerT Handler)
301−moviesHandler =
302− MoviesAPI
303− { list = movieListHandler
304− , movie = movieHandler
305− }
306−
307−movieHandler :: MovieId -> MovieAPI (AsServerT Handler)
308−movieHandler movieId = MovieAPI
309− { get = getMovieHandler movieId
310− , update = updateMovieHandler movieId
311− , delete = deleteMovieHandler movieId
312− }
313−```
314−As you might have noticed, we build our handlers out of the same record types we used to define our API: `MoviesAPI` and `MovieAPI`. What kind of magic is this ?
315−
316−Remember the `mode` type parameter we saw earlier? Since we need to transform our API type into a _server_, we need to provide a server `mode`, which is `AsServerT Handler` here.
317−
318−Finally, we can run the server and connect the API routes to the handlers as usual:
319−
320−``` haskell
321−api :: Proxy MovieCatalogAPI
322−api = Proxy
323−
324−main :: IO ()
325−main = run 8081 app
326−
327−app :: Application
328−app = serve api server
329−
330−```
331−Yay! That’s done and we’ve got our server!
332−
333−## The Client
334−
335−The client, so to speak, is very easy to implement:
336−
337−``` haskell
338−movieCatalogClient :: API (AsClientT ClientM)
339−movieCatalogClient = client api -- remember: api: Proxy MovieCatalogAPI
340−```
341−
342−Have you noticed the `mode` `AsClient ClientM`?
343−
344−We’ve also introduced some operators that help navigate through the nested records.
345−
346−`(//)` is used to jump from one record to another.
347−`(/:)` is used to provide a parameter, whether it be a query param or a capture.
348−
349−Let’s use those nice helpers for our movie catalog:
350−
351−```haskell
352−listMovies :: Maybe SortBy -> ClientM [Movie]
353−listMovies sortBy = movieCatalogClient // movies // list /: sortBy
354−
355−getMovie :: MovieId -> ClientM (Maybe Movie)
356−getMovie movieId = movieCatalogClient // movies // movie /: movieId // get
357−
358−updateMovie :: MovieId -> Movie -> ClientM NoContent
359−updateMovie movieId newMovie = movieCatalogClient // movies // movie /: movieId // update /: newMovie
360−
361−deleteMovie :: MovieId -> ClientM NoContent
362−deleteMovie movieId = movieCatalogClient // movies // movie /: movieId // delete
363−```
364−
365−Done! We’ve got our client!
366−
367−## Conclusion
368−
369−We hope that you found this workbook helpful, and that you now feel more confident using the `NamedRoutes` technique.
370−
371−If you are interested in further understanding the built-in Servant combinators, see [Structuring APIs](https://docs.servant.dev/en/stable/cookbook/structuring-apis/StructuringApis.html).
372−
373−Since `NamedRoutes` is based on the Generic mechanism, you might want to have a look at [Sandy Maguire’s _Thinking with Types_ book](https://doku.pub/download/sandy-maguire-thinking-with-typesz-liborgpdf-4lo5ne7kdj0x).
doc/cookbook/namedRoutes/namedRoutes.cabaldeleted−27
@@ -1,27 +0,0 @@
1−name: namedRoutes
2−version: 0.1
3−synopsis: NamedRoutes - Generic servant API implementation cookbook example
4−homepage: http://docs.servant.dev/
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.6.5, GHC==8.8.3, GHC ==8.10.1
12−
13−executable namedRoutes
14− main-is: NamedRoutes.lhs
15− build-depends: base == 4.*
16− , aeson >= 1.2
17− , text
18− , servant
19− , servant-client
20− , servant-client-core
21− , servant-server
22− , wai >= 3.2
23− , warp >= 3.2
24−
25− default-language: Haskell2010
26− ghc-options: -Wall -pgmL markdown-unlit
27− build-tool-depends: markdown-unlit:markdown-unlit