Open tfc opened 3 years ago
Hi!
What information do you want to add to your API?
Generally, the method to do this is to introduce your own combinator, say WithSomeInfo
. And then define an instance:
instance HasOpenApi api => HasOpenApi (WithSomeInfo :> api) where
toOpenApi _ = toOpenApi @api Proxy
& description ?~ "Something" -- for example
Actually, some combinators from Servant, like Summary
and Description
, already have such instances. You can see a list in the haddocks: https://hackage.haskell.org/package/servant-openapi3-2.0.1.1/docs/Servant-OpenApi.html#t:HasOpenApi
Right... Summary
and Description
are perfect, not only for generating the openapi stuff but also servant docs - very useful and much more straightforward than the approach that i asked for, thank you!
Now i am experimenting with UVerb
s to describe how the api reacts to success/failure (and what kind of failure), but that does not appear to be trivial and i am not sure if that is a servant- or a servant-openapi problem. How would you build an api path whose servant-openapi gives a description for the 200, 404, xxx http status codes and bodies that it could have?
Basically, you replace Get
or Post
with this:
type Api = UVerb 'GET '[JSON] '[ResOK, ResError]
data ResOK = ...
deriving HasStatus via (WithStatus 200 ResOK)
data ResOK = ...
deriving HasStatus via (WithStatus 400 ResError)
I personally like this style, but alternatively you can do UVerb 'GET '[JSON] '[WithStatus 200 ResOK, WithStatus 400 ResError]
.
Note that currently UVerb
requires that status codes of variants are unique. servant-openapi3
will automatically support such apis. You can see more in upstream servant
and servant-server
docs.
Also, it is possible to make combinators injecting results to whole sub-apis, in style of WithSomeInfo
I've shown above, for example to add common 401 responses in auth handler. Take a look at addResponse
or smth like that in servant-openapi3
internal modules, I can't remember the exact name at the moment.
Hi,
i see where your suggestions are going, but i was not successful in getting them to run, yet.
let me show you where i am with an example:
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
module MyApi where
import Data.Aeson
import Data.Aeson.Casing
import Data.OpenApi
import Data.Proxy (Proxy (..))
import Data.Text (Text)
import qualified Data.Text as Text
import GHC.Generics
import Servant.API
import Servant.Docs
-- for openapi things
import Control.Lens
import Servant.OpenApi
-- these imports are for the `Foo` thing
import Data.Typeable (Typeable)
import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)
-- for pretty printing the api
import Data.Aeson.Encode.Pretty
import qualified Data.ByteString.Lazy.Char8 as BSL
data Thing = Thing
{ thingName :: String
, thingColor :: String
, thingWeightGrams :: Int
} deriving (Eq, Show, Generic)
instance ToJSON Thing where
toJSON = genericToJSON $ aesonPrefix snakeCase
instance FromJSON Thing where
parseJSON = genericParseJSON $ aesonPrefix snakeCase
instance ToSample Thing where
toSamples _ = singleSample $ Thing "shoe" "brown" 500
type ThingApi = ThingCreate
type ThingCreate = Summary "Create a thing"
:> "create"
:> ReqBody '[JSON] Thing
:> PostNoContent
-- :> UVerb 'POST '[JSON] '[WithStatus 200 ResOK, WithStatus 400 ResError]
data ResOK = ResOK String
deriving Generic
data ResError = ResError String
deriving Generic
thingApi :: Proxy ThingApi
thingApi = Proxy
instance ToSchema Thing where
declareNamedSchema proxy = genericDeclareNamedSchema defaultSchemaOptions proxy
& mapped.schema.description ?~ "Description of a thing"
instance ToSchema ResOK where
declareNamedSchema proxy = genericDeclareNamedSchema defaultSchemaOptions proxy
& mapped.schema.description ?~ "Success Response"
instance ToSchema ResError where
declareNamedSchema proxy = genericDeclareNamedSchema defaultSchemaOptions proxy
& mapped.schema.description ?~ "Error Response"
thingOpenApi :: OpenApi
thingOpenApi = toOpenApi thingApi
& info.title .~ "Thing API"
& info.version .~ "1.0"
& info.description ?~ "This API is about things"
main = BSL.putStrLn $ encodePretty thingOpenApi
looking at the openapi output, this gives me this:
...
"paths": {
"/create": {
"post": {
"summary": "Create a thing",
"responses": {
"400": {
"description": "Invalid `body`"
},
"204": {
"description": ""
}
},
...
this is great already, but it's still desirable to add text to the description
field of the empty 204 response, as described in the openapi3 docs: https://swagger.io/docs/specification/describing-responses/#empty, e.g. "thing was created".
Also, it would be nice to have a different error description than "invalid body
".
so i understand that to get both, i can simply use UVerb
to "overload" those http codes like this, following your advise:
:> Uverb 'POST [WithStatus 200 ResOK, WithStatus 400 ResError]
(somehow i don't understand how to combine this with NoContent
semantics)
this way the compiler wants ToSchema
instances of ResOK
and ResError
, where i either end up having a content
field which has its description in the schema description at the other part of the generated openapi document, or it just doesn't compile (e.g. description ?~ "description"
is not accepted and i don't understand from reading the code what the right way is).
It would kind of help me best if there was something like:
:> Uverb 'POST [
WithStatus 204 (Description "Thing was created" NoContent),
WithStatus 403 (Description "Such un-thing-like things are simply not allowed" NoContent),
WithStatus 418 (Description "Turns out i am just a teapot. Sorry." NoContent)
]
which then generates an openapi spec like this:
...
"paths": {
"/create": {
"post": {
"summary": "Create a thing",
"responses": {
"403": {
"description": "Such un-thing-like things are simply not allowed"
},
"418": {
"description": "Turns out i am just a teapot. Sorry."
},
"204": {
"description": "Thing was created"
}
},
...
isn't that possible somehow?
@maksbotan i continued experimenting... so far i came up with the following:
type ThingCreate = Summary "Create a thing"
:> "create"
:> ReqBody '[JSON] Thing
:> UVerb 'POST '[] '[
WithStatus 200 (NoContentDescribed "item creation successful"),
WithStatus 300 (NoContentDescribed "something something"),
]
data NoContentDescribed (desc :: Symbol)
instance (AllAccept cs, KnownNat status, OpenApiMethod method, KnownSymbol desc)
=> HasOpenApi (Verb method status cs (NoContentDescribed desc)) where
toOpenApi _ = toOpenApi (Proxy :: Proxy (Verb method status cs (Headers '[] NoContent)))
-- idea is to "attach" description after passing this through to normal nocontent answers which contain empty description
& description .~ (something with desc)
Here i run into the problem, that servant-openapi3/src/Servant/OpenApi/Internal.hs
defines the HasOpenApi
instance for the UVerb
type, but adds ToSchema a
for the items in the list, which is bad in this case because NoContent
is meant to have no schema. so i would not even be able to use UVerb 'POST '[] '[NoContent]
alone.
Am i trying something that is completely unnatural (adding description
to empty responses without a schema), or would this be done completely differently?
hi there,
regarding the example https://github.com/biocad/servant-openapi3/blob/master/example/src/Todo.hs
i have an API, that looks like this, if you adapt the example code:
can i now describe those 3 functions in my openapi just using the types
GetTodoList
,PostNewTodo
, andGetTodo
?i am looking for something like (function
f
would be just what i need, but it's just a rough example):there are examples which do similar things in the openapi3 docs which access the paths via
& paths .~ [("/some-path", ...description....)]
, but this would mean that i would need to duplicate the path information from the types. That would be unelegant as i would no longer have all paths defined in my central API definition.Is there a way? I am a lens newbie, so i might not have discovered the obvious way if there is one.