Skip to content

Commit

Permalink
[chore] some changes as requested by PR reviewers
Browse files Browse the repository at this point in the history
  • Loading branch information
MangoIV committed Apr 26, 2024
1 parent 8bde7de commit 8574daa
Show file tree
Hide file tree
Showing 2 changed files with 33 additions and 32 deletions.
9 changes: 5 additions & 4 deletions doc/cookbook/generic/Generic.lhs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This cookbook explains how to implement an API with a simple record-based
structure. We only deal with non-nested APIs in which every endpoint is on the same
level.

If a you need nesting because you have different branches in your API tree, you
If you need nesting because you have different branches in your API tree, you
might want to jump directly to the [Record-based APIs: the nested records
case](../namedRoutes/NamedRoutes.html) cookbook that broaches the subject.

Expand All @@ -25,7 +25,7 @@ apiHandler :: ServerT API1 Handler
apiHandler = getMovies
:<|> getVersion
```
GHC could scold you with a very tedious message such as :
GHC can and will scold you with a very tedious message such as :
```console
• Couldn't match type 'Handler NoContent'
with 'Movie -> Handler NoContent'
Expand All @@ -50,6 +50,7 @@ GHC could scold you with a very tedious message such as :
getMovieHandler :<|> updateMovieHandler :<|> deleteMovieHandler
|
226 | server = versionHandler
|
```
On the contrary, with the record-based technique, we refer to the routes by their name:
```haskell,ignore
Expand All @@ -58,7 +59,7 @@ data API mode = API
, delete :: "delete" :> ...
}
```
and GHC follows the lead :
and GHC follows the lead:
```console
• Couldn't match type 'NoContent' with 'Movie'
Expected type: AsServerT Handler :- Delete '[JSON] Movie
Expand All @@ -77,6 +78,7 @@ and GHC follows the lead :
delete = deleteMovieHandler movieId}
|
252 | , delete = deleteMovieHandler movieId
|
```
So, records are more readable for a human, and GHC gives you more accurate error messages.
Expand All @@ -92,7 +94,6 @@ module Main (main, api, getLink, routesLinks, cliGet) where
import Control.Exception (throwIO)
import Control.Monad.Trans.Reader (ReaderT, runReaderT)
import Data.Proxy (Proxy (..))
import Network.Wai.Handler.Warp (run)
import System.Environment (getArgs)
Expand Down
56 changes: 28 additions & 28 deletions doc/cookbook/namedRoutes/NamedRoutes.lhs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Record-based APIs: the nested records case
# Record-based APIs: the nested case

*Available in Servant 0.19 or higher*

Expand All @@ -7,8 +7,7 @@ Servant offers a very natural way of constructing APIs with nested records, call
This cookbook explains how to implement such nested-record-based-APIs using
`NamedRoutes` through the example of a Movie Catalog.
If you don't need the nested aspect of the record-based API, you might want to look at [Record-based
APIs: the simple
case](../generic/Generic.html) cookbook
APIs: the simple case](../generic/Generic.html) cookbook
which covers a simpler implementation in which every endpoint is on the same
level.

Expand All @@ -17,13 +16,13 @@ After, we show you how to implement the API type with the NamedRoutes records.
Lastly, we make a Server and a Client out of the API type.

However, it should be understood that this cookbook does _not_ dwell on the
built-in servant combinators as the [Structuring APIs
](<../structuring-apis/StructuringApis.html>) cookbook already covers that angle.
built-in servant combinators as the [Structuring APIs](<../structuring-apis/StructuringApis.html>)
cookbook already covers that angle.


## Boilerplate time!

First, let’s get rid of the the extensions and imports boilerplate in order to focus on our new technique:
First, let’s get rid of the extensions and imports boilerplate in order to focus on our new technique:


```haskell
Expand Down Expand Up @@ -51,7 +50,7 @@ import Servant.Client ( AsClientT, ClientM, client
, (//), (/:) )
import Servant.Client.Generic ()
import Servant.Server ( Application, ServerT )
import Servant.Server ( Application )
import Servant.Server.Generic ( AsServer )
```
Expand All @@ -62,7 +61,7 @@ Now that we’ve handled the boilerplate, we can dive into our Movie Catalog dom
Consider a `Movie` constructed from a `Title` and a `Year` of publication.
``` haskell
```haskell
data Movie = Movie
{ movieId :: MovieId
, title :: Title
Expand Down Expand Up @@ -107,7 +106,7 @@ So, the URLs would look like
Now that we have a very clear idea of the API we want to make, we need to transform it into usable Haskell code:
``` haskell
```haskell
data API mode = API
{ version :: mode :- "version" :> Get '[JSON] Version
Expand All @@ -129,7 +128,7 @@ The `mode` type parameter indicates into which implementation the record’s `Ge
Let's jump into the "movies" subtree node:
``` haskell
```haskell
data MoviesAPI mode = MoviesAPI
{ list :: mode :- "list" :> QueryParam "SortBy" SortBy :> Get '[JSON] [Movie]
Expand All @@ -154,7 +153,7 @@ In this subtree, we illustrated both an endpoint with a **query param** and also
So, let's go deeper into our API tree.
``` haskell
```haskell
data MovieAPI mode = MovieAPI
{ get :: mode :- Get '[JSON] (Maybe Movie)
, update :: mode :- ReqBody '[JSON] Movie :> Put '[JSON] NoContent
Expand All @@ -167,7 +166,7 @@ As you can see, we end up implementing the deepest routes of our API.
Small detail: as our main API tree is also a record, we need the `NamedRoutes` helper.
To improve readability, we suggest you create a type alias:
``` haskell
```haskell
type MovieCatalogAPI = NamedRoutes API
```
Expand Down Expand Up @@ -199,8 +198,8 @@ getMovieHandler :: MovieId -> Handler (Maybe Movie)
getMovieHandler requestMovieId = go moviesDB
where
go [] = pure Nothing
go (movie:ms) | movieId movie == requestMovieId = pure $ Just movie
go (m:ms) = go ms
go (m : _ms) | movieId m == requestMovieId = pure $ Just m
go (_m : ms) = go ms

updateMovieHandler :: MovieId -> Movie -> Handler NoContent
updateMovieHandler requestedMovieId newMovie =
Expand All @@ -211,7 +210,6 @@ deleteMovieHandler :: MovieId -> Handler NoContent
deleteMovieHandler _ =
-- delete the movie from the database...
pure NoContent

```
And assemble them together with the record structure, which is the glue here.
Expand All @@ -232,17 +230,17 @@ moviesHandler =
}
movieHandler :: MovieId -> MovieAPI AsServer
movieHandler movieId = MovieAPI
{ get = getMovieHandler movieId
, update = updateMovieHandler movieId
, delete = deleteMovieHandler movieId
movieHandler mId = MovieAPI
{ get = getMovieHandler mId
, update = updateMovieHandler mId
, delete = deleteMovieHandler mId
}
```
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 ?
Finally, we can run the server and connect the API routes to the handlers as usual:
``` haskell
```haskell
api :: Proxy MovieCatalogAPI
api = Proxy
Expand All @@ -251,15 +249,14 @@ main = run 8081 app
app :: Application
app = serve api server
```
Yay! That’s done and we’ve got our server!
## The Client
The client, so to speak, is very easy to implement:
``` haskell
```haskell
movieCatalogClient :: API (AsClientT ClientM)
movieCatalogClient = client api -- remember: api :: Proxy MovieCatalogAPI
```
Expand All @@ -276,21 +273,24 @@ listMovies :: Maybe SortBy -> ClientM [Movie]
listMovies sortBy = movieCatalogClient // movies // list /: sortBy
getMovie :: MovieId -> ClientM (Maybe Movie)
getMovie movieId = movieCatalogClient // movies // movie /: movieId // get
getMovie mId = movieCatalogClient // movies // movie /: mId // get
updateMovie :: MovieId -> Movie -> ClientM NoContent
updateMovie movieId newMovie = movieCatalogClient // movies // movie /: movieId // update /: newMovie
updateMovie mId newMovie = movieCatalogClient // movies // movie /: mId // update /: newMovie
deleteMovie :: MovieId -> ClientM NoContent
deleteMovie movieId = movieCatalogClient // movies // movie /: movieId // delete
deleteMovie mId = movieCatalogClient // movies // movie /: mId // delete
```
Done! We’ve got our client!
## Conclusion
We hope that you found this cookbook helpful, and that you now feel more confident using the record-based APIs, nested or not.
We hope that you found this cookbook helpful, and that you now feel more confident
using the record-based APIs, nested or not.
If you are interested in further understanding the built-in Servant combinators, see [Structuring APIs](../structuring-apis/StructuringApis.html).
If you are interested in further understanding the built-in Servant combinators, see
[Structuring APIs](../structuring-apis/StructuringApis.html).
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).
Since `NamedRoutes` is based on the Generic mechanism, you might want to have a look at
[Sandy Maguire’s _Thinking with Types_ book](https://thinkingwithtypes.com/).

0 comments on commit 8574daa

Please sign in to comment.