Created
December 31, 2018 09:36
-
-
Save divarvel/61c00a023a7fed71c676898188994fd6 to your computer and use it in GitHub Desktop.
A tale of servant clients
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
{-# LANGUAGE DataKinds #-} | |
{-# LANGUAGE DeriveAnyClass #-} | |
{-# LANGUAGE DeriveGeneric #-} | |
{-# LANGUAGE DerivingStrategies #-} | |
{-# LANGUAGE FlexibleContexts #-} | |
{-# LANGUAGE FlexibleInstances #-} | |
{-# LANGUAGE MultiParamTypeClasses #-} | |
{-# LANGUAGE RankNTypes #-} | |
{-# LANGUAGE RecordWildCards #-} | |
{-# LANGUAGE TemplateHaskell #-} | |
{-# LANGUAGE TypeApplications #-} | |
{-# LANGUAGE TypeFamilies #-} | |
{-# LANGUAGE TypeOperators #-} | |
module Lib | |
( showRoutes | |
, API | |
) where | |
import Control.Category ((>>>)) | |
import Control.Monad.IO.Class (MonadIO, liftIO) | |
import Control.Monad.Reader (MonadReader, ask) | |
import Data.Aeson | |
import qualified Data.Text.IO as TIO | |
import qualified Generics.SOP as SOP | |
import qualified GHC.Generics as GHC | |
import Network.Wai | |
import Network.Wai.Handler.Warp | |
import Servant | |
import Servant.Client | |
type UserId = Int | |
type API = | |
BasicAuth "user-management" User :> | |
"users" :> UsersAPI | |
type UsersAPI = | |
Get '[JSON] [User] | |
:<|> ReqBody '[JSON] UserData :> Post '[JSON] NoContent | |
:<|> Capture "userId" UserId :> UserAPI | |
type UserAPI = | |
Get '[JSON] User | |
:<|> ReqBody '[JSON] UserData :> Put '[JSON] NoContent | |
:<|> Delete '[JSON] NoContent | |
data User = User | |
{ userId :: Int | |
, userFirstName :: String | |
, userLastName :: String | |
} | |
deriving stock (Eq, Show, GHC.Generic) | |
deriving anyclass (FromJSON, ToJSON) | |
data UserData = UserData | |
{ firstName :: String | |
, lastName :: String | |
} | |
deriving stock (Eq, Show, GHC.Generic) | |
deriving anyclass (FromJSON, ToJSON) | |
api :: Proxy API | |
api = Proxy | |
-- Manual client extraction via patter matching | |
listUsersClient :: BasicAuthData -> ClientM [User] | |
listUsersClient auth = | |
let lu :<|> _ = client api auth | |
in lu | |
editUserClient :: BasicAuthData -> UserId -> UserData -> ClientM NoContent | |
editUserClient auth userId = | |
let _ :<|> _ :<|> ue = client api auth | |
_ :<|> eu :<|> _ = ue userId | |
in eu | |
-- Generic client derivation | |
type APIClient = BasicAuthData -> UsersAPIClient | |
data UsersAPIClient = UsersAPIClient | |
{ listUsers :: ClientM [User] | |
, createUser :: UserData -> ClientM NoContent | |
, withUser :: UserId -> UserAPIClient | |
} | |
deriving stock (GHC.Generic) | |
deriving anyclass (SOP.Generic) | |
-- declare the correspondence between the record and | |
-- the API type (UsersAPI / UsersAPIClient) | |
instance (Client ClientM UsersAPI ~ client) | |
=> ClientLike client UsersAPIClient | |
data UserAPIClient = UserAPIClient | |
{ getUser :: ClientM User | |
, editUser :: UserData -> ClientM NoContent | |
, deleteUser :: ClientM NoContent | |
} | |
deriving stock (GHC.Generic) | |
deriving anyclass (SOP.Generic) | |
-- declare the correspondence between the record and | |
-- the API type (UserAPI / UserAPIClient) | |
instance (Client ClientM UserAPI ~ client) | |
=> ClientLike client UserAPIClient | |
newClient :: APIClient | |
newClient = mkClient $ client api | |
listUsersClient' :: BasicAuthData -> ClientM [User] | |
listUsersClient' auth = listUsers $ newClient auth | |
editUserClient' :: BasicAuthData | |
-> UserId -> UserData | |
-> ClientM NoContent | |
editUserClient' auth userId userData = | |
editUser (withUser (newClient auth) userId) userData | |
editUserClient'' :: BasicAuthData | |
-> UserId -> UserData | |
-> ClientM NoContent | |
editUserClient'' auth userId userData = | |
($ userData) . editUser . ($ userId) . withUser $ newClient auth | |
editUserClientWithWildCards :: BasicAuthData | |
-> UserId -> UserData | |
-> ClientM NoContent | |
editUserClientWithWildCards auth userId userData = | |
let UsersAPIClient{..} = newClient auth | |
UserAPIClient{..} = withUser userId | |
in editUser userData | |
withParam :: a -> (a -> b) -> b | |
withParam = flip ($) | |
(//) :: (a -> b) -> (b -> c) -> (a -> c) | |
(//) = flip (.) | |
type App a = | |
forall m. (MonadReader (BasicAuthData, ClientEnv) m, MonadIO m) => m a | |
runClient :: (UsersAPIClient -> ClientM a) | |
-> App (Either ServantError a) | |
runClient f = do | |
(auth, clientEnv) <- ask | |
liftIO $ runClientM (f $ newClient auth) clientEnv | |
editUserClient''' :: UserId -> UserData | |
-> App (Either ServantError NoContent) | |
editUserClient''' userId userData = | |
runClient $ | |
withUser >>> withParam userId >>> editUser >>> withParam userData | |
-- infix used | |
-- (`withUser` userId) >>> (`editUser` userData) | |
-- with fancy alias and dollar sections | |
-- withUser // ($ userId) // editUser // ($ userData) | |
showRoutes :: IO () | |
showRoutes = TIO.putStrLn $ layoutWithContext api ctx | |
where | |
ctx = buggy :. EmptyContext | |
buggy :: BasicAuthCheck User | |
buggy = undefined |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment