Search code examples
haskelltype-level-computationservant

How to create route that will trigger on any path in Servant?


I have a type level function (aka type family) in a Haskell Servant App which takes a Symbol and produces a Type (Route) i.e.

type family AppRoute (x :: Symbol) where
    AppRoute x = x :> Get '[HTML] RawHtml

This is expected to be used in an API:

type ServerAPI = 
    Get '[HTML] RawHtml 
    :<|> UserAPI
    :<|> AdminAPI
    :<|> AppRoute "about" 
    :<|> AppRoute "contact" 
    :<|> AppRoute "services"
    :<|> AppRoute "blog"
    :<|> AppRoute "products"

The corresponding server function is

server :: Server ServerAPI
server = 
    html
    :<|> userServer
    :<|> adminServer
    :<|> html
    :<|> html
    :<|> html
    :<|> html
    :<|> html

Essentially all the AppRoutes go to the same endpoint (raw html file). Is there a way to eliminate the duplication (referring to the last five routes) by writing something like (this does not compile)

type family AppRoute where
    AppRoute = String :> Get '[HTML] RawHtml

type ServerAPI =
    Get '[HTML] RawHtml
    :<|> UserAPI
    :<|> AdminAPI
    :<|> AppRoute _  -- * the problem is here. One function is needed here

with a corresponding server

server :: Server ServerAPI
server = 
    html
    :<|> userServer
    :<|> adminServer
    :<|> html

So in effect, AppRoutes is a type level function that takes any string and returns a route.

In summary, instead of writing

:<|> AppRoute "about" :<|> AppRoute "contact" :<|> AppRoute "services" :<|> AppRoute "products"

I want to be able to write just :<|> AppRoute _


Solution

  • You may use Capture to capture any path. However, it will need to be preceded by : char. So for example

    type AppRoute = Capture "routePath" String :> Get '[HTML] RawHtml
    
    type ServerAPI =
        Get '[HTML] RawHtml
        :<|> UserAPI
        :<|> AdminAPI
        :<|> AppRoute
    

    Now, AppRoute will trigger on yourserver.com/:thisIsMyPath/ and pass "thisIsMyPath" as an argument for the endpoint. I have currently no idea how to bypass this :. Assuming that html is an endpoint that won't depend on given path at this moment, you may define your whole server as

    server :: Server ServerAPI
    server = html
      :<|> userServer
      :<|> adminServer
      :<|> const html
    

    You may read about it here.


    Btw, why not use type alias instead of taking tanky type families? In my Servant apps I usually do

    type AppRoute (x :: Symbol) = x :> Get '[HTML] RawHtml
    

    Which works perfectly.