on master help sign in
Merge pull request #1538 from akhesaCaro/reverting

Reverting NamedRoutes cookbook

commit7ef9730f77bd2a20c9c5b3effce1b4126c79b8f7 authorCaroline GAUDREAU <caroline.gaudreau@gmail.com> dateFri 18 Feb 2022, 12:37 (5 years ago) parent50355d06da8488
4 files changed+152−386
cabal.project+1−1 doc/cookbook/generic/Generic.lhs+141 doc/cookbook/namedRoutes/namedRoutes.cabal → doc/cookbook/generic/generic.cabal+10−12 doc/cookbook/namedRoutes/NamedRoutes.lhs−373
cabal.project+1−1
@@ -34,6 +34,7 @@ packages:
3434 doc/cookbook/db-postgres-pool
3535 doc/cookbook/db-sqlite-simple
3636 doc/cookbook/file-upload
37+ doc/cookbook/generic
3738 doc/cookbook/hoist-server-with-context
3839 doc/cookbook/https
3940 doc/cookbook/jwt-and-basic-auth
@@ -46,7 +47,6 @@ packages:
4647 doc/cookbook/using-custom-monad
4748 doc/cookbook/using-free-client
4849 -- doc/cookbook/open-id-connect
49− doc/cookbook/namedRoutes
5050
5151tests: True
5252optimization: False
doc/cookbook/generic/Generic.lhsnew file+141
@@ -0,0 +1,141 @@
1+# Using generics
2+
3+```haskell
4+{-# LANGUAGE DataKinds #-}
5+{-# LANGUAGE DeriveGeneric #-}
6+{-# LANGUAGE RankNTypes #-}
7+{-# LANGUAGE TypeOperators #-}
8+module Main (main, api, getLink, routesLinks, cliGet) where
9+
10+import Control.Exception (throwIO)
11+import Control.Monad.Trans.Reader (ReaderT, runReaderT)
12+import Data.Proxy (Proxy (..))
13+import Network.Wai.Handler.Warp (run)
14+import System.Environment (getArgs)
15+
16+import Servant
17+import Servant.Client
18+
19+import Servant.API.Generic
20+import Servant.Client.Generic
21+import Servant.Server.Generic
22+```
23+
24+The usage is simple, if you only need a collection of routes.
25+First you define a record with field types prefixed by a parameter `route`:
26+
27+```haskell
28+data Routes route = Routes
29+ { _get :: route :- Capture "id" Int :> Get '[JSON] String
30+ , _put :: route :- ReqBody '[JSON] Int :> Put '[JSON] Bool
31+ }
32+ deriving (Generic)
33+```
34+
35+Then we'll use this data type to define API, links, server and client.
36+
37+## API
38+
39+You can get a `Proxy` of the API using `genericApi`:
40+
41+```haskell
42+api :: Proxy (ToServantApi Routes)
43+api = genericApi (Proxy :: Proxy Routes)
44+```
45+
46+It's recommended to use `genericApi` function, as then you'll get
47+better error message, for example if you forget to `derive Generic`.
48+
49+## Links
50+
51+The clear advantage of record-based generics approach, is that
52+we can get safe links very conveniently. We don't need to define endpoint types,
53+as field accessors work as proxies:
54+
55+```haskell
56+getLink :: Int -> Link
57+getLink = fieldLink _get
58+```
59+
60+We can also get all links at once, as a record:
61+
62+```haskell
63+routesLinks :: Routes (AsLink Link)
64+routesLinks = allFieldLinks
65+```
66+
67+## Client
68+
69+Even more power starts to show when we generate a record of client functions.
70+Here we use `genericClientHoist` function, which lets us simultaneously
71+hoist the monad, in this case from `ClientM` to `IO`.
72+
73+```haskell
74+cliRoutes :: Routes (AsClientT IO)
75+cliRoutes = genericClientHoist
76+ (\x -> runClientM x env >>= either throwIO return)
77+ where
78+ env = error "undefined environment"
79+
80+cliGet :: Int -> IO String
81+cliGet = _get cliRoutes
82+```
83+
84+## Server
85+
86+Finally, probably the most handy usage: we can convert record of handlers into
87+the server implementation:
88+
89+```haskell
90+record :: Routes AsServer
91+record = Routes
92+ { _get = return . show
93+ , _put = return . odd
94+ }
95+
96+app :: Application
97+app = genericServe record
98+
99+main :: IO ()
100+main = do
101+ args <- getArgs
102+ case args of
103+ ("run":_) -> do
104+ putStrLn "Starting cookbook-generic at http://localhost:8000"
105+ run 8000 app
106+ -- see this cookbook below for custom-monad explanation
107+ ("run-custom-monad":_) -> do
108+ putStrLn "Starting cookbook-generic with a custom monad at http://localhost:8000"
109+ run 8000 (appMyMonad AppCustomState)
110+ _ -> putStrLn "To run, pass 'run' argument: cabal new-run cookbook-generic run"
111+```
112+
113+## Using generics together with a custom monad
114+
115+If your app uses a custom monad, here's how you can combine it with
116+generics.
117+
118+```haskell
119+data AppCustomState =
120+ AppCustomState
121+
122+type AppM = ReaderT AppCustomState Handler
123+
124+apiMyMonad :: Proxy (ToServantApi Routes)
125+apiMyMonad = genericApi (Proxy :: Proxy Routes)
126+
127+getRouteMyMonad :: Int -> AppM String
128+getRouteMyMonad = return . show
129+
130+putRouteMyMonad :: Int -> AppM Bool
131+putRouteMyMonad = return . odd
132+
133+recordMyMonad :: Routes (AsServerT AppM)
134+recordMyMonad = Routes {_get = getRouteMyMonad, _put = putRouteMyMonad}
135+
136+-- natural transformation
137+nt :: AppCustomState -> AppM a -> Handler a
138+nt s x = runReaderT x s
139+
140+appMyMonad :: AppCustomState -> Application
141+appMyMonad state = genericServeT (nt state) recordMyMonad
doc/cookbook/namedRoutes/namedRoutes.cabal → doc/cookbook/generic/generic.cabalrenamed+10−12
@@ -1,27 +1,25 @@
1−name: namedRoutes
1+cabal-version: 2.2
2+name: cookbook-generic
23version: 0.1
3−synopsis: NamedRoutes - Generic servant API implementation cookbook example
4+synopsis: Using custom monad to pass a state between handlers
45homepage: http://docs.servant.dev/
5−license: BSD3
6+license: BSD-3-Clause
67license-file: ../../../servant/LICENSE
78author: Servant Contributors
89maintainer: haskell-servant-maintainers@googlegroups.com
910build-type: Simple
10−cabal-version: >=1.10
11−tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.1
11+tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
1212
13−executable namedRoutes
14− main-is: NamedRoutes.lhs
13+executable cookbook-using-custom-monad
14+ main-is: Generic.lhs
1515 build-depends: base == 4.*
16− , aeson >= 1.2
17− , text
1816 , servant
1917 , servant-client
2018 , servant-client-core
2119 , servant-server
22− , wai >= 3.2
20+ , base-compat
2321 , warp >= 3.2
24−
22+ , transformers >= 0.3
2523 default-language: Haskell2010
2624 ghc-options: -Wall -pgmL markdown-unlit
27− build-tool-depends: markdown-unlit:markdown-unlit
25+ build-tool-depends: markdown-unlit:markdown-unlit >= 0.4
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).