1
votes

I build a web-API using servant that grows increasingly large.

I am aware of two ways to automatically create documentation for the api.

First, there's haddock. Haddock turns my code into hyperlinked HTML-pages. Neat! This is especially helpful, because my api endpoints tend to stretch out over several modules and now I can browse through them and find relevant type information.

However, haddock doesn't exactly have a way of displaying lines like these properly:

type Public =
       "new"    :> ReqBody '[JSON] UserNewRequest    :> Post '[JSON] UserNewResponse
  :<|> "exists" :> ReqBody '[JSON] UserExistsRequest :> Post '[JSON] Bool
  :<|> "login"  :> ReqBody '[JSON] LoginRequest      :> Post '[JSON] LoginResponse

Haddock turns it into something like this:

type Public = ("new" :> (ReqBody '[JSON] UserNewRequest :> Post '[JSON] UserNewResponse)) :<|> (("exists" :> (ReqBody '[JSON] UserExistsRequest :> Post '[JSON] Bool)) :<|> ("login" :> (ReqBody '[JSON] LoginRequest :> Post '[JSON] LoginResponse)))

... even adding parentheses. Ironically, formatting is prettier in the code, simply because of the line breaks.

Second, there ist servant-docs. However, servant-docs quite consistently builds a documentation of the endpoints, with nice hooks to add examples that are shown e.g. in JSON. Servant-docs doesn't aim to provide haskell type information - which is all I am after.


So either, I find a way to have haddock display long types in a pretty way, OR I find a way to display haskell types with servant-docs.

In both cases, it doesn't seem to fit their designs. I might need something else entirely.


What I tried already with haddock:

type Public =

  -- create new user
       "new"    :> ReqBody '[JSON] UserNewRequest    :> Post '[JSON] UserNewResponse

  -- check if user exists
  :<|> "exists" :> ReqBody '[JSON] UserExistsRequest :> Post '[JSON] Bool

  -- user login
  :<|> "login"  :> ReqBody '[JSON] LoginRequest      :> Post '[JSON] LoginResponse

It's valid haskell, but the comments are ignored by haddock. Using haddock title syntax --| or -- * results in haddock compile errors.

1
Can you just split the type into multiple types? e.g. type Route1 = ..; type Route2 = ..; type Public = Route1 :<|> Route2. - user2407038
@user2407038 yes, you can. That's an answer. - Zeta
@user2407038 Indeed, that would work - at the expense of added boilerplate. - ruben.moor
Yes, I'm afraid this is the best solution. Then you can document each route in haddock with the usual -- |-style docs, since you now have one toplevel "thing" per endpoint. - Alp Mestanogullari

1 Answers

2
votes

Using per endpoint type aliases was mentioned in comments. However, newer servant has Servant.API.Generic (read more at https://haskell-servant.readthedocs.io/en/stable/cookbook/generic/Generic.html) which let's you write your API in more structured way:

data Public route = Public

  -- | create new user
   { routeNewUser :: route :- "new"    :> ReqBody '[JSON] UserNewRequest    :> Post '[JSON] UserNewResponse

  -- | check if user exists
  , routeExists   :: route :- "exists" :> ReqBody '[JSON] UserExistsRequest :> Post '[JSON] Bool

  -- | user login
  , routeLogin    :: route :- "login"  :> ReqBody '[JSON] LoginRequest      :> Post '[JSON] LoginResponse
  }

This approach is a bit more tricky with nested APIs, but it has a lot of benefits in "linear" apis.