1

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?
The Oddler
  • 6,314
  • 7
  • 51
  • 94

2 Answers2

3

The definition of :> is

data (path :: k) :> (a :: *)

Notice that the right part must have kind *, also known as Type. This is the kind of "normal" Haskell types like Int or Bool that have lifted values.

However, in the type-level expression BackendPrefix :> "auth" the kind of "auth" is Symbol, which is the kind of type-level strings. The kinds don't match and that causes the type error.

(You might wonder, how come that paths like "foo" :> "bar" :> Post '[JSON] User do work? The reason is that a fully applied Post has kind Type, a fully applied :> also has kind Type, and :> associates to the right, like "foo" :> ("bar" :> Post '[JSON] User), so it all checks out.)

The solution? Perhaps give AuthPrefix a type parameter, like

type AuthPrefix restofpath = BackendPrefix :> "auth" :> restofpath

That way we won't end the route fragment in a Symbol.

We have to use AuthPrefix a little differently now:

 AuthPrefix ("email" :> ReqBody '[JSON] AuthEmailRequest :> PostNoContent)

This is because the new definition already takes care of applying the :> to the rest of the path.

danidiaz
  • 26,936
  • 4
  • 45
  • 95
1

:> 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