Compare commits

...

8 Commits

Author SHA1 Message Date
akhesaCaro 1ca98aa840 removing mode explaination 2022-02-24 15:10:05 +01:00
akhesaCaro 53f722d41c Adding paragraph to generic cookbook 2022-02-24 12:35:33 +01:00
akhesaCaro ddfbac9dfc why paragraph, gone 2022-02-24 12:08:57 +01:00
akhesaCaro 2defee6b26 alp comments 2022-02-23 07:42:17 +01:00
akhesaCaro 19e432c33e put some consistency between NamedRoutes cookbook and Generic cookbook 2022-02-23 07:21:39 +01:00
akhesaCaro ece559ee6e renaming route into mode (for clarity and homogenization) 2022-02-23 07:19:01 +01:00
akhesaCaro 485b2530c1 Introducing NamedRoutes cookbook 2022-02-23 07:18:53 +01:00
akhesaCaro 94fab6b29b adding namedRoutes cookbook to cabal 2022-02-21 11:39:07 +01:00
6 changed files with 431 additions and 9 deletions

View File

@ -35,6 +35,7 @@ packages:
doc/cookbook/db-sqlite-simple
doc/cookbook/file-upload
doc/cookbook/generic
doc/cookbook/namedRoutes
doc/cookbook/hoist-server-with-context
doc/cookbook/https
doc/cookbook/jwt-and-basic-auth

View File

@ -1,4 +1,87 @@
# Using generics
# Record-based APIs: the simple case
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
might want to jump directly to the [Record-based APIs: the nested records
case](../namedRoutes/NamedRoutes.html) cookbook that broaches the subject.
Shall we begin?
## Why would I want to use `Records` over the alternative `:<|>` operator?
With a record-based API, we dont need to care about the declaration order of the endpoints.
For example, with the `:<|>` operator theres room for error when the order of the API type
```haskell,ignore
type API1 = "version" :> Get '[JSON] Version
:<|> "movies" :> Get '[JSON] [Movie]
```
does not follow the `Handler` implementation order
```haskell,ignore
apiHandler :: ServerT API1 Handler
apiHandler = getMovies
:<|> getVersion
```
GHC could scold you with a very tedious message such as :
```console
• Couldn't match type 'Handler NoContent'
with 'Movie -> Handler NoContent'
Expected type: ServerT MovieCatalogAPI Handler
Actual type: Handler Version
:<|> ((Maybe SortBy -> Handler [Movie])
:<|> ((MovieId -> Handler (Maybe Movie))
:<|> ((MovieId -> Movie -> Handler NoContent)
:<|> (MovieId -> Handler NoContent))))
• In the expression:
versionHandler
:<|>
movieListHandler
:<|>
getMovieHandler :<|> updateMovieHandler :<|> deleteMovieHandler
In an equation for 'server':
server
= versionHandler
:<|>
movieListHandler
:<|>
getMovieHandler :<|> updateMovieHandler :<|> deleteMovieHandler
|
226 | server = versionHandler
```
On the contrary, with the record-based technique, we refer to the routes by their name:
```haskell,ignore
data API mode = API
{ list :: "list" :> ...
, delete :: "delete" :> ...
}
```
and GHC follows the lead :
```console
• Couldn't match type 'NoContent' with 'Movie'
Expected type: AsServerT Handler :- Delete '[JSON] Movie
Actual type: Handler NoContent
• In the 'delete' field of a record
In the expression:
MovieAPI
{get = getMovieHandler movieId,
update = updateMovieHandler movieId,
delete = deleteMovieHandler movieId}
In an equation for 'movieHandler':
movieHandler movieId
= MovieAPI
{get = getMovieHandler movieId,
update = updateMovieHandler movieId,
delete = deleteMovieHandler movieId}
|
252 | , delete = deleteMovieHandler movieId
```
So, records are more readable for a human, and GHC gives you more accurate error messages.
What are we waiting for?
```haskell
{-# LANGUAGE DataKinds #-}
@ -22,12 +105,12 @@ import Servant.Server.Generic
```
The usage is simple, if you only need a collection of routes.
First you define a record with field types prefixed by a parameter `route`:
First you define a record with field types prefixed by a parameter `mode`:
```haskell
data Routes route = Routes
{ _get :: route :- Capture "id" Int :> Get '[JSON] String
, _put :: route :- ReqBody '[JSON] Int :> Put '[JSON] Bool
data Routes mode = Routes
{ _get :: mode :- Capture "id" Int :> Get '[JSON] String
, _put :: mode :- ReqBody '[JSON] Int :> Put '[JSON] Bool
}
deriving (Generic)
```
@ -110,7 +193,7 @@ main = do
_ -> putStrLn "To run, pass 'run' argument: cabal new-run cookbook-generic run"
```
## Using generics together with a custom monad
## Using record-based APIs together with a custom monad
If your app uses a custom monad, here's how you can combine it with
generics.
@ -121,9 +204,6 @@ data AppCustomState =
type AppM = ReaderT AppCustomState Handler
apiMyMonad :: Proxy (ToServantApi Routes)
apiMyMonad = genericApi (Proxy :: Proxy Routes)
getRouteMyMonad :: Int -> AppM String
getRouteMyMonad = return . show
@ -139,3 +219,4 @@ nt s x = runReaderT x s
appMyMonad :: AppCustomState -> Application
appMyMonad state = genericServeT (nt state) recordMyMonad
```

View File

@ -19,6 +19,7 @@ you name it!
structuring-apis/StructuringApis.lhs
generic/Generic.lhs
namedRoutes/NamedRoutes.lhs
https/Https.lhs
db-mysql-basics/MysqlBasics.lhs
db-sqlite-simple/DBConnection.lhs

View File

@ -0,0 +1,296 @@
# Record-based APIs: the nested records case
*Available in Servant 0.19 or higher*
Servant offers a very natural way of constructing APIs with nested records, called `NamedRoutes`.
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
which covers a simpler implementation in which every endpoint is on the same
level.
First, we start by constructing the domain types of our Movie Catalog.
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.
## Boilerplate time!
First, lets get rid of the the extensions and imports boilerplate in order to focus on our new technique:
```haskell
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeOperators #-}
import GHC.Generics ( Generic )
import Data.Aeson ( FromJSON, ToJSON )
import Data.Proxy ( Proxy(..) )
import Network.Wai.Handler.Warp ( run )
import Servant ( NamedRoutes
, Handler, serve )
import Servant.API (Capture, Delete, Get, Put, QueryParam, ReqBody
, JSON, NoContent (..)
, FromHttpApiData (..),ToHttpApiData(..)
, (:>) )
import Servant.API.Generic ( (:-) )
import Servant.Client ( AsClientT, ClientM, client
, (//), (/:) )
import Servant.Client.Generic ()
import Servant.Server ( Application, ServerT )
import Servant.Server.Generic ( AsServer )
```
## Domain context
Now that weve handled the boilerplate, we can dive into our Movie Catalog domain.
Consider a `Movie` constructed from a `Title` and a `Year` of publication.
``` haskell
data Movie = Movie
{ movieId :: MovieId
, title :: Title
, year :: Year
}
deriving stock Generic
deriving anyclass (FromJSON, ToJSON)
type MovieId = String
type Title = String
type Year = Int
```
Lets forget about the deriving stuff for now and think about the API that we want to make.
```
"version" -> Get Version
/
api "list" -> Get [Movie] ?sortBy= Title | Year (sort by the Title or the Year)
\ /
"movies" Get Movie
\ /
Capture MovieId - Put Movie
\
Delete MovieId
```
In this example, we create a very simple endpoint for the Version,
and several complex endpoints that use nested records for the CRUD part of the movie.
So, the URLs would look like
- GET …/version
- GET …/movies/list?sortby=Title
- GET …/movies/<MovieId>/
- PUT …/movies/<MovieId>/
- DELETE …/movies/<MovieId>
### API Type
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
data API mode = API
{ version :: mode :- "version" :> Get '[JSON] Version
, movies :: mode :- "movies" :> NamedRoutes MoviesAPI
} deriving stock Generic
type Version = String -- This will do for the sake of example.
```
Here, we see the first node of our tree. It contains the two branches “version” and “movies” respectively:
The “version” branch is very simple and self-explanatory.
The “movies” branch will contain another node, represented by another record (see above). That is why we need the `NameRoutes` helper.
Note:
The `mode` type parameter indicates into which implementation the records `Generic` representation will be transformed—as a client or as a server. We will discuss that later.
Let's jump into the "movies" subtree node:
``` haskell
data MoviesAPI mode = MoviesAPI
{ list :: mode :- "list" :> QueryParam "SortBy" SortBy :> Get '[JSON] [Movie]
, movie :: mode :- Capture "movieId" MovieId :> NamedRoutes MovieAPI
} deriving stock Generic
data SortBy = Year | Title
instance ToHttpApiData SortBy where
toQueryParam Year = "year"
toQueryParam Title = "title"
instance FromHttpApiData SortBy where
parseQueryParam "year" = Right Year
parseQueryParam "title" = Right Title
parseQueryParam param = Left $ param <> " is not a valid value"
```
So, remember, this type represents the `MoviesAPI` node that weve connected earlier to the main `API` tree.
In this subtree, we illustrated both an endpoint with a **query param** and also, a **capture** with a subtree underneath it.
So, let's go deeper into our API tree.
``` haskell
data MovieAPI mode = MovieAPI
{ get :: mode :- Get '[JSON] (Maybe Movie)
, update :: mode :- ReqBody '[JSON] Movie :> Put '[JSON] NoContent
, delete :: mode :- Delete '[JSON] NoContent
} deriving stock Generic
```
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
type MovieCatalogAPI = NamedRoutes API
```
That's it, we have our `MovieCatalogAPI` type!
Let's make a server and a client out of it!
## The Server
As you know, we cant talk about a server, without addressing the handlers.
First, we take our handlers…
```haskell
versionHandler :: Handler Version
versionHandler = pure "0.0.1"
movieListHandler :: Maybe SortBy -> Handler [Movie]
movieListHandler _ = pure moviesDB
moviesDB :: [Movie]
moviesDB =
[ Movie "1" "Se7en" 1995
, Movie "2" "Minority Report" 2002
, Movie "3" "The Godfather" 1972
]
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
updateMovieHandler :: MovieId -> Movie -> Handler NoContent
updateMovieHandler requestedMovieId newMovie =
-- update the movie list in the database...
pure NoContent
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.
```haskell
server :: API AsServer
server =
API
{ version = versionHandler
, movies = moviesHandler
}
moviesHandler :: MoviesAPI AsServer
moviesHandler =
MoviesAPI
{ list = movieListHandler
, movie = movieHandler
}
movieHandler :: MovieId -> MovieAPI AsServer
movieHandler movieId = MovieAPI
{ get = getMovieHandler movieId
, update = updateMovieHandler movieId
, delete = deleteMovieHandler movieId
}
```
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
api :: Proxy MovieCatalogAPI
api = Proxy
main :: IO ()
main = run 8081 app
app :: Application
app = serve api server
```
Yay! Thats done and weve got our server!
## The Client
The client, so to speak, is very easy to implement:
``` haskell
movieCatalogClient :: API (AsClientT ClientM)
movieCatalogClient = client api -- remember: api :: Proxy MovieCatalogAPI
```
Weve also introduced some operators that help navigate through the nested records.
`(//)` is used to jump from one record to another.
`(/:)` is used to provide a parameter, whether it be a query param or a capture.
Lets use those nice helpers for our movie catalog:
```haskell
listMovies :: Maybe SortBy -> ClientM [Movie]
listMovies sortBy = movieCatalogClient // movies // list /: sortBy
getMovie :: MovieId -> ClientM (Maybe Movie)
getMovie movieId = movieCatalogClient // movies // movie /: movieId // get
updateMovie :: MovieId -> Movie -> ClientM NoContent
updateMovie movieId newMovie = movieCatalogClient // movies // movie /: movieId // update /: newMovie
deleteMovie :: MovieId -> ClientM NoContent
deleteMovie movieId = movieCatalogClient // movies // movie /: movieId // delete
```
Done! Weve 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.
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 Maguires _Thinking with Types_ book](https://doku.pub/download/sandy-maguire-thinking-with-typesz-liborgpdf-4lo5ne7kdj0x).

View File

@ -0,0 +1,27 @@
name: namedRoutes
version: 0.1
synopsis: NamedRoutes - Generic servant API implementation cookbook example
homepage: http://docs.servant.dev/
license: BSD3
license-file: ../../../servant/LICENSE
author: Servant Contributors
maintainer: haskell-servant-maintainers@googlegroups.com
build-type: Simple
cabal-version: >=1.10
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.1
executable namedRoutes
main-is: NamedRoutes.lhs
build-depends: base == 4.*
, aeson >= 1.2
, text
, servant
, servant-client
, servant-client-core
, servant-server
, wai >= 3.2
, warp >= 3.2
default-language: Haskell2010
ghc-options: -Wall -pgmL markdown-unlit
build-tool-depends: markdown-unlit:markdown-unlit

View File

@ -35,6 +35,11 @@ The first part, `FactoringAPI`, shows how we can
endpoints, just like we turn `a * b + a * c` into
`a * (b + c)` in algebra.
(It should be noted that the `(:<|>)` operator is not the only way of combining
endpoints with Servant. Other techniques are shown in subsequent cookbooks. See
[record-based alternative for implementing APIs](StructuringApis.html#record-based-alternative-for-implementing-apis))
``` haskell
-- Two endpoints:
-- - GET /x/<some 'Int'>[?y=<some 'Int'>]
@ -203,3 +208,14 @@ main = run 8080 . serve api $
This program is available as a cabal project
[here](https://github.com/haskell-servant/servant/tree/master/doc/cookbook/structuring-apis).
## Record-based alternative for implementing APIs
It should be noted that the `(:<|>)` is not the only way of combining endpoints.
Servant offers a convenient way to design APIs with records avoiding the ordering constraint of the operator.
A simple case is approached in the [Record-based APIs: the simple
case](../generic/Generic.html)
cookbook, which deals with flat APIs where every endpoint is on the same level.
Also, a more complex example with nested record is discussed in [Record-based APIs: the nested
records case](../namedRoutes/NamedRoutes.html) in which we implement an API tree with many branches.