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:
- What does the above error mean, and can I use something like
type AuthPrefix = BackendPrefix :> "auth"to create a more complex prefix? - Is there a way to prefix some routes with one prefix, and the other routes with a different prefix, when using generics in Servant?
CodePudding user response:
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.
CodePudding user response:
:> 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
