Skip to content

Commit 6da8488

Browse files
committed
Revert "removing Generic cookbook in favour of NamedRoutes"
This reverts commit 34aed1d.
1 parent f4cd564 commit 6da8488

File tree

3 files changed

+167
-0
lines changed

3 files changed

+167
-0
lines changed

cabal.project

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

doc/cookbook/generic/Generic.lhs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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/generic/generic.cabal

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
cabal-version: 2.2
2+
name: cookbook-generic
3+
version: 0.1
4+
synopsis: Using custom monad to pass a state between handlers
5+
homepage: http://docs.servant.dev/
6+
license: BSD-3-Clause
7+
license-file: ../../../servant/LICENSE
8+
author: Servant Contributors
9+
maintainer: [email protected]
10+
build-type: Simple
11+
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
12+
13+
executable cookbook-using-custom-monad
14+
main-is: Generic.lhs
15+
build-depends: base == 4.*
16+
, servant
17+
, servant-client
18+
, servant-client-core
19+
, servant-server
20+
, base-compat
21+
, warp >= 3.2
22+
, transformers >= 0.3
23+
default-language: Haskell2010
24+
ghc-options: -Wall -pgmL markdown-unlit
25+
build-tool-depends: markdown-unlit:markdown-unlit >= 0.4

0 commit comments

Comments
 (0)