haskelltype-level-computationservant

Haskell Servant: Construct URL from API


Suppose we have this simple API:

type FooAPI
   = "foo" 
  :> QueryParam "age" Int 
  :> Get '[PlainText] Text

Is there a way to link type-level servant's API with a function that will generate URL for it? Like

someMagicFunction :: Proxy api -> SomeTypeFamily api
someMagicFunction = ?

generateURL :: Maybe Int -> Text
generateURL = someMagicFunction (Proxy @FooAPI)

>>> generateURL (Just 42)
"https://somehost/foo?age=42"
>>> generateURL Nothing
"https://somehost/foo"

I want to increase type-safety of URL generation so if I'll add some new parameter to FooAPI it will immediately appear in the type of generateURL and my code will not compile without editing the calls of generateURL.

I'm aware of servant-client library and I know that client function does somewhat similar but I couldn't figure out how to use this library in my case.


Solution

  • I would say that Servant.Links is exactly what you are looking for.

    In particular, in your case I would use allLinks to generate a Link corresponding to the URL piece containing the path and query parameters:

    >>> linkURI $ allLinks (Proxy @FooAPI) Nothing
    foo
    >>> linkURI $ allLinks (Proxy @FooAPI) (Just 18)
    foo?age=18
    

    Then I would transform the generated Link into an URI where I would specify required scheme and hostname. Here's a fully working module:

    {-# LANGUAGE DataKinds        #-}
    {-# LANGUAGE TypeApplications #-}
    {-# LANGUAGE TypeOperators    #-}
    
    import           Data.Proxy
    import           Data.Text     (Text)
    import           Network.URI
    import           Servant.API
    import           Servant.Links
    
    type FooAPI
       = "foo"
      :> QueryParam "age" Int
      :> Get '[PlainText] Text
    
    generateURL :: Maybe Int -> String
    generateURL age = show uri
      { uriScheme = "https:"
      , uriAuthority = Just nullURIAuth
        { uriRegName = "somehost" }
      , uriPath = "/" <> uriPath uri
      }
      where
        uri = linkURI link
        link = allLinks (Proxy @FooAPI) age
    

    And demonstration:

    >>> generateURL (Just 42)
    "https://somehost/foo?age=42"
    >>> generateURL Nothing
    "https://somehost/foo"