0

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.

voidlizard
  • 805
  • 5
  • 10
  • Is `BaseUrl` something you know in advance? If it is, you don't need to parse it; just supply the fixed, known BaseUrl value when calling the client fn. If not, why can't you parse it too from the text file? It's just composed of a scheme, host, port, and path. It should be decently easy to write up a `Parser BaseUrl` using `parsec` or `megaparsec`. Finally, don't you typically create a new Manager with `newManager :: ManagerSettings -> IO Manager`, and share the created manager instance between requests? – liminalisht Aug 16 '16 at 13:56
  • Let's assume it fixed and there is no task to parse it. And yes, Manager is created by it's typical way. The question is how to generate a parser for request's arguments that may be used in call of ```client``` function. – voidlizard Aug 16 '16 at 14:05
  • Oh I misunderstood. Let me see if I have this right: you're not asking how to parse values from a text file and pass these arguments to a client fn (straightforward); rather, you're asking how to *infer* which client fn to call in the first place based on the (inferred) types of the values parsed from the text file. Is this right? – liminalisht Aug 16 '16 at 14:27
  • I guess that's right.This task may be reduced to intering tuple types of arity infered from API type. – voidlizard Aug 16 '16 at 14:44

0 Answers0