If I have some Servant's API described in servant DSL, like:
type API = "some" :> QueryParam "a" Int :> QueryParam "b" Double:> Get '[JSON] Int
It has HasClient instance, so I have a client:
λ> let theClient = client (Proxy :: Proxy API)
λ> :t theClient
theClient
:: Maybe Int
-> Maybe Double
-> http-client-0.4.31:Network.HTTP.Client.Types.Manager
-> BaseUrl
-> ClientM Int
λ>
It's good if I'm going to call all that API manually. But what if I need to generate HTTP requests from, say, a text file?
I can generate simple parser from the API
using (my own) typeclass
HasParser
which parses all query arguments from text file like this:
get some a=1234 b=0.1234
...
but how to connect the result of parsing with the client's type?
Of course I can write all that parsers on value level for each API
method, but I want to generate all thar parsers from the API
on type level, so for particular API
the parser should generate a function:
Manager -> BaseUrl -> ClientM Int
Is it even possible?
UPD.
Ok, the question actually constist of two:
1) How to generate generate universal client
2) How to generate parser, actually what type use to keep parsing result.
(1) is quite simple although it's quite clunky:
type API = QueryParam "a" Int :> Get '[JSON] Int
type family FinalType k where
FinalType (a :> b) = FinalType b
FinalType (Verb a b c d) = Client (Verb a b c d)
class ConvertAPI api input where
convert :: Proxy api -> input -> FinalType api
instance ( HasClient api
, Client api ~ (Maybe a -> FinalType api)
, Read a
)
=> ConvertAPI api String where
convert (Proxy :: Proxy api) i = (client (Proxy :: Proxy api)) (Just (read i))
instance ( HasClient api
, Client api ~ (Maybe a -> Maybe b -> FinalType api)
, Read a
, Read b
)
=> ConvertAPI api (String,String) where
convert (Proxy :: Proxy api) (i,ii) = (client (Proxy :: Proxy api)) (Just (read i)) (Just (read i))
there are two instances for singleton and tuple of two, but it's easy to extend for tuples of all possible dimensions.
The 2nd question is still unclear, I can't figure out yet how to generate parsers that will produce tuples of different dimensions.
UPD2.
Minimal implementation using nested tuples is here: https://gist.github.com/voidlizard/ef67a7ae486834d591d7b45c493bec1d
it gives:
λ> parse (Proxy :: Proxy API) ["42"]
Just ("42",())
λ> parse (Proxy :: Proxy API2) ["some","42"]
Just ("42",())
λ> parse (Proxy :: Proxy API2) ["42"]
Nothing
So basically it's what I'd like to have, but the implementation is quite ugly. Is it possible to make it better - using some HLists or Vinyl or something else to walk away from that nested tuples ?
UPD3 and conclusion.
Well, the good news that all of is is pretty possible. We may define and implement HasParser type class that defines all parse function and type or data family for parsing results, and ConvertAPI typeclass that will translate Servant's client functions and parsing results into the something that has type [Token] -> Manager -> BaseUrl -> ClientM a
. There are some complexities with type of that a that should be wrapped into an existential type, but it is still possible. Implementation of this is still not perfect, but this is an another issue, how to implement API's with modern features like type-level programming.
The another way of doing this is just construct HTTP request up on Servant.Common.Req on values level, using parser generated from servant's API definition. Is way easier but requires lot of code and somehow duplicates Servant's HasClient instances.
Frankly, we all need some kind of manual how to design API'es using modern type-level features.