parent
abc53b54e3
commit
63e9099f87
5 changed files with 228 additions and 0 deletions
|
@ -40,6 +40,7 @@ packages:
|
||||||
doc/cookbook/using-custom-monad
|
doc/cookbook/using-custom-monad
|
||||||
doc/cookbook/using-free-client
|
doc/cookbook/using-free-client
|
||||||
-- doc/cookbook/open-id-connect
|
-- doc/cookbook/open-id-connect
|
||||||
|
doc/cookbook/openapi3
|
||||||
|
|
||||||
tests: True
|
tests: True
|
||||||
optimization: False
|
optimization: False
|
||||||
|
|
|
@ -19,6 +19,7 @@ you name it!
|
||||||
|
|
||||||
structuring-apis/StructuringApis.lhs
|
structuring-apis/StructuringApis.lhs
|
||||||
generic/Generic.lhs
|
generic/Generic.lhs
|
||||||
|
openapi3/OpenAPI.lhs
|
||||||
https/Https.lhs
|
https/Https.lhs
|
||||||
db-mysql-basics/MysqlBasics.lhs
|
db-mysql-basics/MysqlBasics.lhs
|
||||||
db-sqlite-simple/DBConnection.lhs
|
db-sqlite-simple/DBConnection.lhs
|
||||||
|
|
200
doc/cookbook/openapi3/OpenAPI.lhs
Normal file
200
doc/cookbook/openapi3/OpenAPI.lhs
Normal 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 — 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.
|
26
doc/cookbook/openapi3/openapi3.cabal
Normal file
26
doc/cookbook/openapi3/openapi3.cabal
Normal 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
|
BIN
doc/cookbook/openapi3/swagger-ui-example.png
Normal file
BIN
doc/cookbook/openapi3/swagger-ui-example.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 145 KiB |
Loading…
Reference in a new issue