servant-cli
Parse command line arguments into a servant client, from a servant API, using
optparse-applicative for parsing, displaying help, and auto-completion.
Hooks into the annotation system used by servant-docs to provide descriptions
for parameters and captures.
See example/greet.hs
for a sample program.
Getting started
We’re going to break down the example program in example/greet.hs
.
Here’s a sample API revolving around greeting and some deep paths, with
authentication.
type TestApi =
Summary "Send a greeting"
:> "hello"
:> Capture "name" Text
:> QueryParam "capital" Bool
:> Get '[JSON] Text
:<|> Summary "Greet utilities"
:> "greet"
:> ( Get '[JSON] Int
:<|> Post '[JSON] NoContent
)
:<|> Summary "Deep paths test"
:> "dig"
:> "down"
:> "deep"
:> Summary "Almost there"
:> Capture "name" Text
:> "more"
:> Summary "We made it"
:> Get '[JSON] Text
testApi :: Proxy TestApi
testApi = Proxy
To parse this, we can use parseClient
, which generates a client action that
we can run:
main :: IO ()
c <- parseClient testApi (Proxy :: Proxy ClientM) $
header "greet"
<> progDesc "Greet API"
manager' <- newManager defaultManagerSettings
res <- runClientM c $
mkClientEnv manager' (BaseUrl Http "localhost" 8081 "")
case res of
Left e -> throwIO e
Right r -> putStrLn $ case r of
Left g -> "Greeting: " ++ T.unpack g
Right (Left (Left i)) -> show i ++ " returned"
Right (Left (Right _)) -> "Posted!"
Right (Right s) -> s
Note that parseClient
and other functions all take InfoMod
s from
optparse-applicative, to customize how the top-level --help
is displayed.
The result will be a bunch of nested Either
s for each :<|>
branch and
endpoint. However, this can be somewhat tedious to handle.
With Handlers
The library also offers parseHandleClient
, which accepts nested :<|>
s with
handlers for each endpoint, mirroring the structure of the API:
main :: IO ()
c <- parseHandleClient testApi (Proxy :: Proxy ClientM)
(header "greet" <> progDesc "Greet API") $
(\g -> "Greeting: " ++ T.unpack g)
:<|> ( (\i -> show i ++ " returned")
:<|> (\_ -> "Posted!")
)
:<|> id
manager' <- newManager defaultManagerSettings
res <- runClientM c $
mkClientEnv manager' (BaseUrl Http "localhost" 8081 "")
case res of
Left e -> throwIO e
Right r -> putStrLn r
The handlers essentially let you specify how to sort each potential endpoint’s
response into a single output value.
Clients that need context
Things get slightly more complicated when your client requires something that
can’t be passed in through the command line, such as authentication information
(username, password).
type TestApi =
Summary "Send a greeting"
:> "hello"
:> Capture "name" Text
:> QueryParam "capital" Bool
:> Get '[JSON] Text
:<|> Summary "Greet utilities"
:> "greet"
:> ( Get '[JSON] Int
:<|> BasicAuth "login" Int -- ^ Adding 'BasicAuth'
:> Post '[JSON] NoContent
)
:<|> Summary "Deep paths test"
:> "dig"
:> "down"
:> "deep"
:> Summary "Almost there"
:> Capture "name" Text
:> "more"
:> Summary "We made it"
:> Get '[JSON] Text
For this, you can pass in a context, using parseClientWithContext
or
parseHandleClientWithContext
:
main :: IO ()
c <- parseHandleClientWithContext
testApi
(Proxy :: Proxy ClientM)
(getPwd :& RNil)
(header "greet" <> progDesc "Greet API") $
(\g -> "Greeting: " ++ T.unpack g)
:<|> ( (\i -> show i ++ " returned")
:<|> (\_ -> "Posted!")
)
:<|> id
manager' <- newManager defaultManagerSettings
res <- runClientM c $
mkClientEnv manager' (BaseUrl Http "localhost" 8081 "")
case res of
Left e -> throwIO e
Right r -> putStrLn r
where
getPwd :: ContextFor ClientM (BasicAuth "login" Int)
getPwd = GenBasicAuthData . liftIO $ do
putStrLn "Authentication needed for this action!"
putStrLn "Enter username:"
n <- BS.getLine
putStrLn "Enter password:"
p <- BS.getLine
pure $ BasicAuthData n p