https://github.com/mstksg/servant-cli
Generate a command line client from a servant API
https://github.com/mstksg/servant-cli
Last synced: 3 months ago
JSON representation
Generate a command line client from a servant API
- Host: GitHub
- URL: https://github.com/mstksg/servant-cli
- Owner: mstksg
- License: bsd-3-clause
- Created: 2019-04-25T06:56:28.000Z (about 6 years ago)
- Default Branch: master
- Last Pushed: 2024-01-12T20:31:38.000Z (over 1 year ago)
- Last Synced: 2025-03-01T01:02:00.106Z (4 months ago)
- Language: Haskell
- Homepage: https://hackage.haskell.org/package/servant-cli
- Size: 646 KB
- Stars: 28
- Watchers: 5
- Forks: 4
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# 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.```haskell
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] TexttestApi :: Proxy TestApi
testApi = Proxy
```To parse this, we can use `parseClient`, which generates a client action that
we can run:```haskell
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.With Handlers
-------------The library also offers `parseHandleClient`, which accepts nested `:<|>`s with
handlers for each endpoint, mirroring the structure of the API:```haskell
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!")
)
:<|> idmanager' <- 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).```haskell
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`:```haskell
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!")
)
:<|> idmanager' <- 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
```