-
-
Notifications
You must be signed in to change notification settings - Fork 409
/
Generic.lhs
141 lines (106 loc) · 3.58 KB
/
Generic.lhs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# Using generics
```haskell
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeOperators #-}
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)
import Servant
import Servant.Client
import Servant.API.Generic
import Servant.Client.Generic
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`:
```haskell
data Routes route = Routes
{ _get :: route :- Capture "id" Int :> Get '[JSON] String
, _put :: route :- ReqBody '[JSON] Int :> Put '[JSON] Bool
}
deriving (Generic)
```
Then we'll use this data type to define API, links, server and client.
## API
You can get a `Proxy` of the API using `genericApi`:
```haskell
api :: Proxy (ToServantApi Routes)
api = genericApi (Proxy :: Proxy Routes)
```
It's recommented to use `genericApi` function, as then you'll get
better error message, for example if you forget to `derive Generic`.
## Links
The clear advantage of record-based generics approach, is that
we can get safe links very conviently. We don't need to define endpoint types,
as field accessors work as proxies:
```haskell
getLink :: Int -> Link
getLink = fieldLink _get
```
We can also get all links at once, as a record:
```haskell
routesLinks :: Routes (AsLink Link)
routesLinks = allFieldLinks
```
## Client
Even more power starts to show when we generate a record of client functions.
Here we use `genericClientHoist` function, which let us simultaneously
hoist the monad, in this case from `ClientM` to `IO`.
```haskell
cliRoutes :: Routes (AsClientT IO)
cliRoutes = genericClientHoist
(\x -> runClientM x env >>= either throwIO return)
where
env = error "undefined environment"
cliGet :: Int -> IO String
cliGet = _get cliRoutes
```
## Server
Finally, probably the most handy usage: we can convert record of handlers into
the server implementation:
```haskell
record :: Routes AsServer
record = Routes
{ _get = return . show
, _put = return . odd
}
app :: Application
app = genericServe record
main :: IO ()
main = do
args <- getArgs
case args of
("run":_) -> do
putStrLn "Starting cookbook-generic at http://localhost:8000"
run 8000 app
-- see this cookbook below for custom-monad explanation
("run-custom-monad":_) -> do
putStrLn "Starting cookbook-generic with a custom monad at http://localhost:8000"
run 8000 (appMyMonad AppCustomState)
_ -> putStrLn "To run, pass 'run' argument: cabal new-run cookbook-generic run"
```
## Using generics together with a custom monad
If your app uses a custom monad, here's how you can combine it with
generics.
```haskell
data AppCustomState =
AppCustomState
type AppM = ReaderT AppCustomState Handler
apiMyMonad :: Proxy (ToServantApi Routes)
apiMyMonad = genericApi (Proxy :: Proxy Routes)
getRouteMyMonad :: Int -> AppM String
getRouteMyMonad = return . show
putRouteMyMonad :: Int -> AppM Bool
putRouteMyMonad = return . odd
recordMyMonad :: Routes (AsServerT AppM)
recordMyMonad = Routes {_get = getRouteMyMonad, _put = putRouteMyMonad}
-- natural transformation
nt :: AppCustomState -> AppM a -> Handler a
nt s x = runReaderT x s
appMyMonad :: AppCustomState -> Application
appMyMonad state = genericServeT (nt state) recordMyMonad