Search code examples
haskellservant

Haskell Servant: How to prefix routes when using generics?


I'm using Servant generic, and have a datatype for my routes:

data Routes route = Routes
  { getLiveness :: route :- GetLiveness,
    getReadiness :: route :- GetReadiness,

    getAuthVerifyEmailToken :: route :- GetAuthVerifyEmailToken,
    postAuthEmail :: route :- PostAuthEmail,
    ...
  }
  deriving (Generic)

type BackendPrefix = "backend"

type AuthPrefix = "auth"

type GetLiveness = BackendPrefix :> "liveness" :> Get '[JSON] Text

type GetReadiness = BackendPrefix :> "readiness" :> Get '[JSON] Text

type GetAuthVerifyEmailToken = AuthPrefix :> "verify" :> "email" :> Capture "token" JWT :> RedirectResponse '[PlainText] NoContent

type PostAuthEmail = AuthPrefix :> "email" :> ReqBody '[JSON] AuthEmailRequest :> PostNoContent

The first two use the same prefix "backend", and all other's have an "auth" prefix.

However, I now want to change the "auth" prefix to "backend/auth". So I tried chaging:

type AuthPrefix = BackendPrefix :> "auth"

This results in an error

>     • Expected a type, but
>       ‘"auth"’ has kind
>       ‘ghc-prim-0.6.1:GHC.Types.Symbol’
>     • In the second argument of ‘(:>)’, namely ‘"auth"’
>       In the type ‘BackendPrefix :> "auth"’
>       In the type declaration for ‘AuthPrefix’
>    |
> 34 | type AuthPrefix = BackendPrefix :> "auth"
>    |          

So I googled and found you can do this when not using generic you can do:

type APIv1 = "api" :> "v1" :> API

But I couldn't figure out how to do this with generics.

I guess that leaves two questions:

  1. What does the above error mean, and can I use something like type AuthPrefix = BackendPrefix :> "auth" to create a more complex prefix?
  2. Is there a way to prefix some routes with one prefix, and the other routes with a different prefix, when using generics in Servant?

Solution

  • :> is infixr 4, which means it's right-associative. Consider this type:

    type GetFoo = "backend" :> "auth" :> "foo" :> Get '[JSON] Text
    

    It's interpreted as if you parenthesized it like this:

    type GetFoo = "backend" :> ("auth" :> ("foo" :> Get '[JSON] Text))
    

    With the type synonym you tried, it would get parenthesized like this instead:

    type GetFoo = ("backend" :> "auth") :> ("foo" :> Get '[JSON] Text)
    

    And that's clearly not the same thing, and in fact not even valid.


    To help understand, consider this ordinary Haskell code with no advanced types:

    xs = 1:2:3:4:5:6:[]
    ys = 1:2:4:8:16:32:[]
    

    Now imagine you tried to write this:

    zs = 1:2
    xs = zs:3:4:5:6:[]
    ys = zs:4:8:16:32:[]
    

    The reason it doesn't work is the exact same reason you can't have your type synonym.


    One final example:

    x = 2 ^ 3 ^ 2 -- evaluates to 2 ^ (3 ^ 2) = 512
    
    y = 2 ^ 3
    x = y ^ 2 -- evaluates to (2 ^ 3) ^ 2 = 64