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.
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 ()
main = do
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.
The library also offers parseHandleClient
, which accepts nested :<|>
s with
handlers for each endpoint, mirroring the structure of the API:
main :: IO ()
main = do
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.
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 ()
main = do
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