From 6da8488f9b5bda891eac202e33465f7ce948f0a3 Mon Sep 17 00:00:00 2001 From: akhesaCaro Date: Fri, 18 Feb 2022 12:14:28 +0100 Subject: [PATCH] Revert "removing Generic cookbook in favour of NamedRoutes" This reverts commit 34aed1d28989a8a91ddc64e7aafafdc807d71249. --- cabal.project | 1 + doc/cookbook/generic/Generic.lhs | 141 +++++++++++++++++++++++++++++ doc/cookbook/generic/generic.cabal | 25 +++++ 3 files changed, 167 insertions(+) create mode 100644 doc/cookbook/generic/Generic.lhs create mode 100644 doc/cookbook/generic/generic.cabal diff --git a/cabal.project b/cabal.project index 248be451..95187ef0 100644 --- a/cabal.project +++ b/cabal.project @@ -34,6 +34,7 @@ packages: doc/cookbook/db-postgres-pool doc/cookbook/db-sqlite-simple doc/cookbook/file-upload + doc/cookbook/generic doc/cookbook/hoist-server-with-context doc/cookbook/https doc/cookbook/jwt-and-basic-auth diff --git a/doc/cookbook/generic/Generic.lhs b/doc/cookbook/generic/Generic.lhs new file mode 100644 index 00000000..45180230 --- /dev/null +++ b/doc/cookbook/generic/Generic.lhs @@ -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 diff --git a/doc/cookbook/generic/generic.cabal b/doc/cookbook/generic/generic.cabal new file mode 100644 index 00000000..725f70c9 --- /dev/null +++ b/doc/cookbook/generic/generic.cabal @@ -0,0 +1,25 @@ +cabal-version: 2.2 +name: cookbook-generic +version: 0.1 +synopsis: Using custom monad to pass a state between handlers +homepage: http://docs.servant.dev/ +license: BSD-3-Clause +license-file: ../../../servant/LICENSE +author: Servant Contributors +maintainer: haskell-servant-maintainers@googlegroups.com +build-type: Simple +tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7 + +executable cookbook-using-custom-monad + main-is: Generic.lhs + build-depends: base == 4.* + , servant + , servant-client + , servant-client-core + , servant-server + , base-compat + , warp >= 3.2 + , transformers >= 0.3 + default-language: Haskell2010 + ghc-options: -Wall -pgmL markdown-unlit + build-tool-depends: markdown-unlit:markdown-unlit >= 0.4