Add Cookbook for OpenAPI 3.0

Fixes #1446
This commit is contained in:
Maxim Koltsov 2021-10-24 00:05:01 +02:00
parent abc53b54e3
commit 63e9099f87
5 changed files with 228 additions and 0 deletions

View file

@ -40,6 +40,7 @@ packages:
doc/cookbook/using-custom-monad
doc/cookbook/using-free-client
-- doc/cookbook/open-id-connect
doc/cookbook/openapi3
tests: True
optimization: False

View file

@ -19,6 +19,7 @@ you name it!
structuring-apis/StructuringApis.lhs
generic/Generic.lhs
openapi3/OpenAPI.lhs
https/Https.lhs
db-mysql-basics/MysqlBasics.lhs
db-sqlite-simple/DBConnection.lhs

View file

@ -0,0 +1,200 @@
# OpenAPI
OpenAPI is language-agnostic format for API specifications. It is structured as JSON or YAML
document and can be used to communicate API documentation between the backend and its clients, like
the frontend.
The OpenAPI specification itself is available at https://swagger.io/specification/. It is supported
by various tools, like [swagger-ui](https://swagger.io/tools/swagger-ui/) — a tool that
visualizes OpenAPI document and allows to send requests to the backend it describes, or
[swagger-codegen](https://swagger.io/tools/swagger-codegen/), which can generate client code in a
variety of languages given the specification.
Since Servant backends already contain a comprehensive description of the API they provide, it is
fairly easy to generate OpenAPI specification based on that description. This is achieved with
[servant-openapi3](https://hackage.haskell.org/package/servant-openapi3) package, which is based on
older `servant-swagger`, targeted at second version of OpenAPI specification (then called Swagger).
This cookbook demonstrates how to use `servant-openapi3` and how to integrate interactive schema
browser with your backend.
## The sample API
Let's start with an API of an example TODO service:
```haskell
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
import GHC.Generics
import Data.Text
import Data.Aeson
import Servant
import Data.OpenApi
import Servant.OpenApi
import Servant.Swagger.UI
import Network.Wai.Handler.Warp as Warp
-- | A single Todo entry.
data Todo = Todo
{ created :: Int -- ^ Creation datetime.
, summary :: Text -- ^ Task summary.
}
deriving stock (Show, Generic)
deriving anyclass (ToSchema, ToJSON, FromJSON)
-- | A unique Todo entry ID.
newtype TodoId = TodoId Int
deriving stock (Show, Generic)
deriving newtype (ToJSON, FromHttpApiData)
deriving anyclass (ToParamSchema, ToSchema)
-- | The API of a Todo service.
type TodoAPI
= "todo" :> Description "Get all TODO items"
:> Get '[JSON] [Todo]
:<|> "todo" :> Description "Add a new TODO item"
:> ReqBody '[JSON] Todo :> Post '[JSON] TodoId
:<|> "todo" :> Description "Get a TODO item by its id"
:> Capture "id" TodoId :> Get '[JSON] Todo
:<|> "todo" :> Description "Update an existing TODO item by its id"
:> Capture "id" TodoId :> ReqBody '[JSON] Todo :> Put '[JSON] TodoId
```
Notice that all API endpoints are decorated with `Description` (coming from `servant` itself): these
descriptions will propagate to the OpenAPI document automatically.
## Adding OpenAPI
We are ready to define OpenAPI document for our `TodoAPI`. Everything you need to do for that is to
use `toOpenApi` function from `servant-openapi3` package:
```haskell
-- | OpenAPI spec for Todo API.
todoOpenApi :: OpenApi
todoOpenApi = toOpenApi (Proxy :: Proxy TodoAPI)
```
This is possible since we've derived `ToSchema` for `Todo` and `ToParamSchema` for `TodoId` (needed
since the type is used in URLs) instances &mdash; and this is everything that is needed to generate
the OpenAPI 3.0 specification for our API. All of this is thanks to `Generic`-based schema generator
found in `openapi3` and `servant-openapi3` packages.
Of course, you can customize the schema in many ways, see the documentation for
[`openapi3`](https://hackage.haskell.org/package/openapi3) package.
The generated schema looks something like this:
```json
{
"openapi": "3.0.0",
"info": {
"title": "",
"version": ""
},
"paths": {
"/todo": {
"get": {
"description": "Get all TODO items",
"responses": {
"200": {
"content": {
"application/json;charset=utf-8": {
"schema": {
"items": {
"$ref": "#/components/schemas/Todo"
},
"type": "array"
}
}
},
"description": ""
}
}
}
},
........
}
"components": {
"schemas": {
"Todo": {
"required": [
"created",
"summary"
],
"properties": {
"summary": {
"type": "string"
},
"created": {
"minimum": -9223372036854775808,
"type": "integer",
"maximum": 9223372036854775807
}
},
"type": "object"
},
"TodoId": {
"minimum": -9223372036854775808,
"type": "integer",
"maximum": 9223372036854775807
}
}
}
}
```
The schema can be pasted into the [Swagger editor](https://editor.swagger.io/), which will nicely
display the generated schema.
## Integrating schema browser into the backend
Or, the schema browser can be integrated into the backend itself. This is done via
`servant-swagger-ui` package, which embeds `swagger-ui` into the Servant backend.
First, define a sub-api that will serve the documentation:
```haskell
type DocsAPI = SwaggerSchemaUI "swagger-ui" "swagger.json"
```
And a full API for your backend, which combines your endpoints and `DocsAPI`:
```haskell
type API = DocsAPI :<|> TodoAPI
```
`SwaggerSchemaUI` describes an API that will serve the interactive schema browser at `/swagger-ui`
of your server and the specification in JSON format at `/swagger.json`. Of course, both paths are
customizable.
A handler for `SwaggerSchemaUI`, called `swaggerSchemaUIServer`, expectes one argument: the
specification itself. In our case, it's `todoOpenApi`.
```haskell
todoServer :: Servant.Server API
todoServer = swaggerSchemaUIServer todoOpenApi
:<|> error "The actual TODO API is not implemented"
```
Now the server can be run as usual:
```haskell
main :: IO ()
main = do
Warp.run 5000 $ serve (Proxy :: Proxy API) todoServer
```
Run this example, navigate to http://localhost:5000/swagger-ui and you will see the interactive
schema browser:
![](./swagger-ui-example.png)
You can make requests in this UI and they will be sent to your backend as expected.

View file

@ -0,0 +1,26 @@
name: cookbook-openapi3
version: 2.1
synopsis: OpenAPI 3.0 schema generation example
homepage: http://docs.servant.dev/
license: BSD3
license-file: ../../../servant/LICENSE
author: Servant Contributors
maintainer: haskell-servant-maintainers@googlegroups.com
build-type: Simple
cabal-version: >=1.10
tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
executable cookbook-openapi3
main-is: OpenAPI.lhs
build-tool-depends: markdown-unlit:markdown-unlit
default-language: Haskell2010
ghc-options: -Wall -pgmL markdown-unlit
build-depends: base >= 4.9 && <5
, aeson
, openapi3
, servant
, servant-server
, servant-openapi3
, servant-swagger-ui
, text
, warp

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB