-
-
Notifications
You must be signed in to change notification settings - Fork 414
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1538 from akhesaCaro/reverting
Reverting NamedRoutes cookbook
- Loading branch information
Showing
4 changed files
with
152 additions
and
386 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,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 recommended 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 conveniently. 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 lets 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 |
22 changes: 10 additions & 12 deletions
22
doc/cookbook/namedRoutes/namedRoutes.cabal → doc/cookbook/generic/generic.cabal
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,27 +1,25 @@ | ||
name: namedRoutes | ||
cabal-version: 2.2 | ||
name: cookbook-generic | ||
version: 0.1 | ||
synopsis: NamedRoutes - Generic servant API implementation cookbook example | ||
synopsis: Using custom monad to pass a state between handlers | ||
homepage: http://docs.servant.dev/ | ||
license: BSD3 | ||
license: BSD-3-Clause | ||
license-file: ../../../servant/LICENSE | ||
author: Servant Contributors | ||
maintainer: [email protected] | ||
build-type: Simple | ||
cabal-version: >=1.10 | ||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.1 | ||
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7 | ||
|
||
executable namedRoutes | ||
main-is: NamedRoutes.lhs | ||
executable cookbook-using-custom-monad | ||
main-is: Generic.lhs | ||
build-depends: base == 4.* | ||
, aeson >= 1.2 | ||
, text | ||
, servant | ||
, servant-client | ||
, servant-client-core | ||
, servant-server | ||
, wai >= 3.2 | ||
, base-compat | ||
, warp >= 3.2 | ||
|
||
, transformers >= 0.3 | ||
default-language: Haskell2010 | ||
ghc-options: -Wall -pgmL markdown-unlit | ||
build-tool-depends: markdown-unlit:markdown-unlit | ||
build-tool-depends: markdown-unlit:markdown-unlit >= 0.4 |
Oops, something went wrong.