4
votes

Servant uses Servant.API.safeLink to generate relative URLs, but I'm running into a problem that makes me think I am misunderstanding something basic either about how to use it or about how to define Servant APIs.

The minimal example I've constructed contains two endpoints. One is intended to be a "front door" endpoint at (relative URL) /foo, and the other at /foo/1:

{-# LANGUAGE DataKinds     #-}
{-# LANGUAGE TypeOperators #-}
import Servant

data HTML
type Foo = "foo" :> (Foo0 :<|> Foo1)
type Foo0 = Get '[HTML] String
type Foo1 = "1" :> Get '[HTML] String

slFoo :: Link
slFoo = safeLink (Proxy :: Proxy Foo) (Proxy :: Proxy Foo1)

The definition of slFoo above gives me the error

Could not deduce: IsElem' ("1" :> Get '[HTML] String) ("foo" :> (Foo0 :<|> Foo1))

...which is exactly the kind of error I get when safeLink is asked to produce a link which is not in the API defined by its first parameter. The error is similar when the second parameter to safeLink is Proxy :: Foo0 instead.

I've tried many, many permutations of this and can't seem to figure it out by myself with the use of the documentation I've found. I'd appreciate some pointers that let me figure out where my misunderstanding(s) lie.

1
That's because Foo1 doesn't include the leading foo static path fragment, so when you ask servant to produce a link for Foo1 (within the Foo API), it doesn't find an endpoint that matches Foo1 exactly and complains.Alp Mestanogullari
Maybe something like servant-flatten is appropriate here? Just like the client/server examples in those docs, you could pass a "flattened" version of your Foo API type as safeLink's second argument, and the first argument could perhaps use Nth on the said flattened API type to hit the endpoint you want?Alp Mestanogullari
Regarding uses of servant-flatten, surprisingly I'm not the only user :-) See here.Alp Mestanogullari
Regarding your actual question, the idea is that instead of passing apiProxy :: Proxy YourAPI to whatever function you're using (serve, client, safeLink, ...), you'd pass flatten apiProxy which has type Proxy (Flat YourAPI). You don't have to define an instance of Nth, you just have to use it to get a proxy to the right endpoint. Nth <some number> <some flattened API type> will give you an API type for the <some number> + 1 th endpoint. Nth 0 for the first, Nth 1 for the second, etc. Email me if this is not clear enough, I'm not a huge fan of SO for those matters.Alp Mestanogullari
Indeed, Nth doesn't seem to be used in public repositories. I'd be delighted to merge a patch that adds an example of using Nth to its haddocks. =) Re: github search, I always look there first when I'm looking for examples of using some library/function/..., I definitely recommend it.Alp Mestanogullari

1 Answers

0
votes

The example doesn't work because Foo1, the type you've defined for the endpoint, does not itself contain the full path to itself relative to the top of the Foo API.

One way for you to fix this situation is by using a "flattened" API instead:

safeLink (Proxy :: Proxy (Flat Foo)) (Proxy :: Proxy (Nth 1 (Flat Foo)))

(requiring import Servant.API.Flatten as well)

The disadvantage is that you have to know the ordinal position of Foo1 within Foo. There doesn't seem to be a way to get the answer you want by using the types as specified in your question. You could define the flattened API in the first place, at the cost of making the structure as clear (IMO).

Thanks to Alp Mestanogullari for explaining this to me above, in the comments to the question. He should really get credit for the answer!