Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Description for QueryParam #62

Closed
rikvdkleij opened this issue May 5, 2017 · 20 comments
Closed

Description for QueryParam #62

rikvdkleij opened this issue May 5, 2017 · 20 comments

Comments

@rikvdkleij
Copy link

How can I add description for QueryParam with ToParamSchema? According to Swagger specification it is possible to add description to parameter but I do not see way how to add it.

@fizruk
Copy link
Member

fizruk commented May 28, 2017

@rikvdkleij you can't do it with ToParamSchema since description is a part of a particular parameter, not its schema. E.g. you can have

type MyAPI
    = QueryParam "resource_id" ResourceId :> Get '[JSON] Resource
 :<|> QueryParam "resource_id" ResourceId :> Delete '[JSON] Resource

And you might want to add different descriptions for "resource_id":

ID of a resource to fetch.

ID of a resource to delete.

To attach description currently the best way would be to add new combinator like this:

type MyAPI
    = GetResource :<|> DeleteResource

type GetResource
  = QueryParam "resource_id" ResourceId  --^ "ID of a resource to fetch." 
 :> Get '[JSON] Resource

type DeleteResource
  = QueryParam "resource_id" ResourceId  --^ "ID of a resource to delete."
 :> Delete '[JSON] Resource

data ParamDesc a (desc :: Symbol)
type (--^) = ParamDesc

instance ... => HasServer (ParamDesc str a :> api) where ...
instance ... => HasClient (ParamDesc str a :> api) where ...
instance ... => HasSwagger (ParamDesc str a :> api) where ...

You could also attach descriptions on term-level using optics, but it doesn't seem to be particularly elegant:

-- | Peak into 'Param' of an endpoint by its name.
paramByName :: Text -> Traversal' Operation Param
paramByName paramName = parameters.traverse._Inline.filtered byName
  where
    byName param = paramName == param^.name

-- to add a param description
mySwagger
  & subOperations (Proxy @GetResource) (Proxy @MyAPI).paramByName "resource_id".description ?~ "ID of a resource to fetch."
  & subOperations (Proxy @DeleteResource) (Proxy @MyAPI).paramByName "resource_id".description ?~ "ID of a resource to delete."

@rikvdkleij
Copy link
Author

@fizruk Thanks for your answer. Both solution feel rather "heavy" for adding a description to parameter.

Are you on ZuriHac so we can discuss this further?

@fizruk
Copy link
Member

fizruk commented Jun 2, 2017

@rikvdkleij type-level solution is the right way to go in my opinion. It makes code look nice and complete and it allows different interpretations to use this information. E.g. you could also generate JS client with comments in the generated code.

We're hoping to adopt this at work first, see how it goes and then try push it into Servant's core libraries. In any case I should be able to share some code soon, so you could just copy-paste it to enable this feature.

Unfortunately I'm not going to ZuriHac, but @phadej is and I think he'd be happy to discuss this!

@phadej
Copy link
Contributor

phadej commented Jun 2, 2017

I surely am!

@rikvdkleij
Copy link
Author

@fizruk My opinion is also influenced by I'm not (yet) experienced in type-level programming and optics 😄 Thanks in advance for sharing your code.

@phadej See you there!

@phadej
Copy link
Contributor

phadej commented Jun 2, 2017

I imagine writing API like

type FooEndpoint =
   "foo" :>
   Capture "foo-id" FooId      :? "The identifier" :>
   QueryParam "canonical" Bool :? "Normalise the representation :>
   Get '[JSON] Foo             :? "Why not annotate the result too"

I'm quite sure, that should work:

-- | Server doesn't care about descriptions
instance HasServer (combinator :> api) => HasServer (combinator :? description :> api) where
    ...

@fizruk
Copy link
Member

fizruk commented Jun 2, 2017

@phadej one problem you'll get is for interpretations where you care about descriptions. E.g. in HasDocs, HasSwagger and, probably, HasForeign. With current design you're like to double amount of instances:

instance ... => HasSwagger (QueryParam name a :? description :> api) where ...
instance ... => HasSwagger (QueryParam name a :> api) where ...

instance ... => HasSwagger (Capture name a :? description :> api) where ...
instance ... => HasSwagger (Capture name a :> api) where ...

Another problem is that description is this just one kind of extra info we can add to parameters. Other kinds include leniency, required/optional, parameter name (which is embedded currently into every parameter, but might as well be external). If one day'd want to add more of those, imaging dealing with a parameter like this:

type FooEndpoint
  = "foo"
 :> Required (Lenient (Name "foo-id" (QueryParam FooId))) :? "Foo ID"
 :> Get '[JSON] Foo

How many HasSwagger instances would I need to make this work?
What if I switch/skip options?
What about Required (Optional a) or Required (Required a)?

I think that maybe we'd have to converge to something more flexible, like this:

type FooEndpoint
  = "foo"
 :> Param Query "foo-id" a
      '[ "description" := "ID of a foo you'd like to GET."
       , "required" := True
       , "lenient"  := False
       ...
       ]
 :> Get '[JSON] Foo

I imagine then you'd treat various parameter options (like "required" or "lenient") separately and would be able to easily extend HasSwagger and other interpretations.

P. S. I think all combinators should have prefix form like ParamDescription and infix type synonyms.
I also find --^ for param descriptions very appealing since it mimics Haddock's -- ^.

@phadej
Copy link
Contributor

phadej commented Jun 2, 2017

The type-level record is nice. Was it your (latest) idea for leniency/required too (you have said that you have an idea, but never mentioned it IIRC)?

@phadej
Copy link
Contributor

phadej commented Jun 2, 2017

BTW, I also think that in some interpretations we could use

instance (HasFooModifier mod, HasFoo api) => HasFoo (mod :> api) where

where HasFooModifier is completely independent, or takes the subapi as an argument.

If nothing else, it would make generated haddocks much simpler.

EDIT may also make GHC faster in resolving the instances, as there are less-flexible ones.

@fizruk
Copy link
Member

fizruk commented Jun 2, 2017

@phadej sorry, I can't remember what I was referring to back then :)
But the idea of (extensible) type-level records for parameters as well as endpoints has crossed my mind before! It's just that these ideas are still raw and I'm not sure they don't introduce some other serious usability problems.

The problem with HasFooModifier is that there are different kinds of modifiers. E.g. EndpointDescription desc :> api and QueryParam name a :> api must be handled differently. And this will make methods of HasFooModifier ugly.

Compare this

class HasSwagger api where
  toSwagger :: proxy api -> Swagger

class HasSwaggerParam p where
  toParam :: proxy p -> Param

instance (HasSwaggerParam param, HasSwagger api) => HasSwagger (param :> api) where ...

instance ... => HasSwaggerParam (QueryParam name a) where ...

-- impossible to write this instance!
instance ... => HasSwaggerParam (EndpointDescription desc) where ...

and this

class HasSwagger api where
  toSwagger :: proxy api -> Swagger

-- uglier, but more generic modifiers
-- now modifier has to find what to modify in the entire Swagger spec!
class HasSwaggerModifier mod where
  toModifier :: proxy mod -> Swagger -> Swagger

instance (HasSwaggerModifier mod, HasSwagger api) => HasSwagger (mod :> api) where ...

-- both instances are possible, but are less elegant and harder to write
instance ... => HasSwaggerModifier (QueryParam name a) where ...
instance ... => HasSwaggerModifier (EndpointDescription desc) where ...

We could maybe have different modifiers for different kinds:

class HasSwagger api where
  toSwagger :: proxy api -> Swagger

class HasSwaggerModifier (mod :: k) where
  type Mod (mod :: k) :: *
  toModifier :: proxy mod -> Mod mod

-- we can now have different modifiers kinds!
-- for current servant parameters we can use * kind, to leave it open!
-- note how Symbol kind works out nicely for path segments!
-- we can add new modifiers with new kinds (but they'd be closed)
instance ... => HasSwagger ((mod :: *) :> api) where ...
instance ... => HasSwagger ((mod :: Symbol) :> api) where ...
instance ... => HasSwagger ((mod :: EndpointDescription) :> api) where ...

@phadej
Copy link
Contributor

phadej commented Jun 2, 2017

@fizruk I'm lost with EndpointDescription, what is that?

@fizruk
Copy link
Member

fizruk commented Jun 2, 2017

@phadej EndpointDescription is Description from #60.
For the latest example (with poly-kinded HasSwaggerModifier) it would be something like:

data EndpointDescription = Description Symbol

type FooAPI
  = Description "Get foo by its ID."
 :> Capture "foo-id" FooId
 :> Get '[JSON] Foo

instance KnownSymbol desc => HasSwaggerModifier (Description desc)
  type Mod EndpointDescription = String
  toModifier _ = symbolVal (Proxy @ desc)

instance (HasSwaggerModifier mod, HasSwagger api)
  => HasSwagger ((mod :: EndpointDescription) :> api) wher
  toSwagger = toSwagger (Proxy @ api)
    & allOperations.description ?~ toModifier (Proxy @ mod)

@phadej
Copy link
Contributor

phadej commented Jun 2, 2017

but won't that work equally well with

class HasSwaggerModifier mod where
  modifySwagger :: proxy mod -> Swagger -> Swagger

instance KnownSymbol => HasSwaggerModifier (Description desc) where
   modifySwagger _ swagger = swagger
     & allOperations.description ?~ symbolVal (Proxy :: Proxy desc)

?

@fizruk
Copy link
Member

fizruk commented Jun 2, 2017

@phadej do you mean to restrict HasSwaggerModifier only to kind *?
Otherwise there's a problem of overlapping instances.

@phadej
Copy link
Contributor

phadej commented Jun 2, 2017

Seems that even GHC-7.8.4 can differentiate the instances based on the kind:

{-# LANGUAGE FlexibleInstances, UndecidableInstances #-}
{-# LANGUAGE PolyKinds, DataKinds, TypeOperators, ScopedTypeVariables #-}

import GHC.TypeLits
import Data.Proxy

infixr 5 :>
data a :> b
data Foo = Foo Int deriving Show

class HasFoo a where
    foo :: Proxy a -> Foo

class ModFoo a where
    modFoo :: Proxy a -> Foo -> Foo

instance (ModFoo a, HasFoo b) => HasFoo (a :> b) where
    foo _ = modFoo (Proxy :: Proxy a) (foo (Proxy :: Proxy b))

instance HasFoo Bool where
    foo _ = Foo 2
 
instance KnownSymbol a => ModFoo a where
    modFoo p (Foo x) = Foo $ length (symbolVal p) + x

instance ModFoo () where
    modFoo _ (Foo x) = Foo (x + 1)

{-
*Main> foo (Proxy :: Proxy ("foo" :> () :> Bool))
Foo 6
-}

@fizruk
Copy link
Member

fizruk commented Jun 4, 2017

@phadej my point was that Proxy a -> Foo -> Foo is unnecessarily generic/complex.

It also gets worse with associated type families or more complex methods:

class HasServer api where
  type ServerT api (m :: * -> *) :: *
  route :: Proxy api -> Context context -> Delayed env (Server api) -> Router env

class ModServer m where
  type ModServerT m :: ?
  modRoute :: ?

Instead, if we limit our ambitions per kind, we can make it simpler:

-- | Parameters for server handlers.
-- Only works for kind *.
class ServerParam (p :: *) where
  type ParamType p :: *
  addParamCheck :: proxy p -> Delayed env (ParamType p -> b) -> Delayer env b

-- | General instance for parameters.
instance (ServerParam p, HasServer api) => HasServer (param :> api) context where
  type ServerT (param :> api) m = ParamType param -> ServerT api m

  route _ context subserver =
    route (Proxy :: Proxy api) context (addParamCheck (Proxy :: Proxy param) subserver)

And it's now easier to write individual instances for parameters:

-- | Sample instance for 'QueryParam'.
instance ... => ServerParam (QueryParam sym a) where
  type ParamType (QueryParam sym a) = Maybe a
  addParamCheck _ subserver = subserver `addParameterCheck` withRequest parseParam
    where parseParam req = ...

-- same approach would work for many other combinators
instance ... => ServerParam (QueryFlag sym) where ...
instance ... => ServerParam (Header sym a) where ...
instance ... => ServerParam (ReqBody cts a) where ...
instance ... => ServerParam (BasicAuth realm usr) where ...
instance ... => ServerParam HttpVersion where ...

-- only doesn't work for CaptureAll (for existing combinators),
-- but that's easily fixed by giving it its own kind and it's own HasServer instance
-- I guess it can also work with {-# OVERLAPPING #-},
-- but kind-dispatching has more appeal to me

data CaptureAll = CaptureAll Symbol Type

instance ... => HasServer (CaptureAll sym a :> api) where ...

I also think that introducing ServerParam like above does not just make things easier, but also allows for combinators like Required to be implemented universally for all parameter combinators. But I can't sketch an implementation easily, so maybe I'm wrong here. I do believe though that we can tailor ServerParam in a such way that Required (and similar combinators) is possible.

@phadej
Copy link
Contributor

phadej commented Mar 11, 2018

@fizruk per kind makes sense indeed.

@phadej
Copy link
Contributor

phadej commented Mar 11, 2018 via email

@phadej phadej closed this as completed Nov 13, 2018
fisx pushed a commit to wireapp/servant-swagger that referenced this issue Jun 1, 2019
…ding_members

Notify team contacts on user update
@dcastro
Copy link

dcastro commented Aug 21, 2022

@phadej is there a way of adding a Description to QueryParams? QueryParams doesn't seem to take a mods parameter.

@phadej
Copy link
Contributor

phadej commented Aug 21, 2022

I don't work on servant anymore.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants