git.iko.soy/typeable/servant
on master help sign in
Add Cookbook for OpenAPI 3.0
63e9099Maxim Koltsov
cabal.project+1
@@ -40,6 +40,7 @@ packages:
40 doc/cookbook/using-custom-monad
40 doc/cookbook/using-custom-monad
41 doc/cookbook/using-free-client
41 doc/cookbook/using-free-client
42 -- doc/cookbook/open-id-connect
42 -- doc/cookbook/open-id-connect
43+ doc/cookbook/openapi3
43
44
44tests: True
45tests: True
45optimization: False
46optimization: False
doc/cookbook/index.rst+1
@@ -19,6 +19,7 @@ you name it!
19
19
20 structuring-apis/StructuringApis.lhs
20 structuring-apis/StructuringApis.lhs
21 generic/Generic.lhs
21 generic/Generic.lhs
22+ openapi3/OpenAPI.lhs
22 https/Https.lhs
23 https/Https.lhs
23 db-mysql-basics/MysqlBasics.lhs
24 db-mysql-basics/MysqlBasics.lhs
24 db-sqlite-simple/DBConnection.lhs
25 db-sqlite-simple/DBConnection.lhs
doc/cookbook/openapi3/OpenAPI.lhsnew file+200
@@ -0,0 +1,200 @@
1+# OpenAPI
2+
3+OpenAPI is language-agnostic format for API specifications. It is structured as JSON or YAML
4+document and can be used to communicate API documentation between the backend and its clients, like
5+the frontend.
6+
7+The OpenAPI specification itself is available at https://swagger.io/specification/. It is supported
8+by various tools, like [swagger-ui](https://swagger.io/tools/swagger-ui/) — a tool that
9+visualizes OpenAPI document and allows to send requests to the backend it describes, or
10+[swagger-codegen](https://swagger.io/tools/swagger-codegen/), which can generate client code in a
11+variety of languages given the specification.
12+
13+Since Servant backends already contain a comprehensive description of the API they provide, it is
14+fairly easy to generate OpenAPI specification based on that description. This is achieved with
15+[servant-openapi3](https://hackage.haskell.org/package/servant-openapi3) package, which is based on
16+older `servant-swagger`, targeted at second version of OpenAPI specification (then called Swagger).
17+
18+This cookbook demonstrates how to use `servant-openapi3` and how to integrate interactive schema
19+browser with your backend.
20+
21+## The sample API
22+
23+Let's start with an API of an example TODO service:
24+
25+```haskell
26+{-# LANGUAGE DataKinds #-}
27+{-# LANGUAGE TypeOperators #-}
28+{-# LANGUAGE DeriveGeneric #-}
29+{-# LANGUAGE DeriveAnyClass #-}
30+{-# LANGUAGE DerivingStrategies #-}
31+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
32+
33+import GHC.Generics
34+import Data.Text
35+import Data.Aeson
36+
37+import Servant
38+
39+import Data.OpenApi
40+import Servant.OpenApi
41+import Servant.Swagger.UI
42+
43+import Network.Wai.Handler.Warp as Warp
44+
45+-- | A single Todo entry.
46+data Todo = Todo
47+ { created :: Int -- ^ Creation datetime.
48+ , summary :: Text -- ^ Task summary.
49+ }
50+ deriving stock (Show, Generic)
51+ deriving anyclass (ToSchema, ToJSON, FromJSON)
52+
53+-- | A unique Todo entry ID.
54+newtype TodoId = TodoId Int
55+ deriving stock (Show, Generic)
56+ deriving newtype (ToJSON, FromHttpApiData)
57+ deriving anyclass (ToParamSchema, ToSchema)
58+
59+-- | The API of a Todo service.
60+type TodoAPI
61+ = "todo" :> Description "Get all TODO items"
62+ :> Get '[JSON] [Todo]
63+ :<|> "todo" :> Description "Add a new TODO item"
64+ :> ReqBody '[JSON] Todo :> Post '[JSON] TodoId
65+ :<|> "todo" :> Description "Get a TODO item by its id"
66+ :> Capture "id" TodoId :> Get '[JSON] Todo
67+ :<|> "todo" :> Description "Update an existing TODO item by its id"
68+ :> Capture "id" TodoId :> ReqBody '[JSON] Todo :> Put '[JSON] TodoId
69+```
70+
71+Notice that all API endpoints are decorated with `Description` (coming from `servant` itself): these
72+descriptions will propagate to the OpenAPI document automatically.
73+
74+## Adding OpenAPI
75+
76+We are ready to define OpenAPI document for our `TodoAPI`. Everything you need to do for that is to
77+use `toOpenApi` function from `servant-openapi3` package:
78+
79+```haskell
80+-- | OpenAPI spec for Todo API.
81+todoOpenApi :: OpenApi
82+todoOpenApi = toOpenApi (Proxy :: Proxy TodoAPI)
83+```
84+
85+This is possible since we've derived `ToSchema` for `Todo` and `ToParamSchema` for `TodoId` (needed
86+since the type is used in URLs) instances &mdash; and this is everything that is needed to generate
87+the OpenAPI 3.0 specification for our API. All of this is thanks to `Generic`-based schema generator
88+found in `openapi3` and `servant-openapi3` packages.
89+
90+Of course, you can customize the schema in many ways, see the documentation for
91+[`openapi3`](https://hackage.haskell.org/package/openapi3) package.
92+
93+The generated schema looks something like this:
94+
95+```json
96+{
97+ "openapi": "3.0.0",
98+ "info": {
99+ "title": "",
100+ "version": ""
101+ },
102+ "paths": {
103+ "/todo": {
104+ "get": {
105+ "description": "Get all TODO items",
106+ "responses": {
107+ "200": {
108+ "content": {
109+ "application/json;charset=utf-8": {
110+ "schema": {
111+ "items": {
112+ "$ref": "#/components/schemas/Todo"
113+ },
114+ "type": "array"
115+ }
116+ }
117+ },
118+ "description": ""
119+ }
120+ }
121+ }
122+ },
123+ ........
124+ }
125+ "components": {
126+ "schemas": {
127+ "Todo": {
128+ "required": [
129+ "created",
130+ "summary"
131+ ],
132+ "properties": {
133+ "summary": {
134+ "type": "string"
135+ },
136+ "created": {
137+ "minimum": -9223372036854775808,
138+ "type": "integer",
139+ "maximum": 9223372036854775807
140+ }
141+ },
142+ "type": "object"
143+ },
144+ "TodoId": {
145+ "minimum": -9223372036854775808,
146+ "type": "integer",
147+ "maximum": 9223372036854775807
148+ }
149+ }
150+ }
151+}
152+```
153+
154+The schema can be pasted into the [Swagger editor](https://editor.swagger.io/), which will nicely
155+display the generated schema.
156+
157+## Integrating schema browser into the backend
158+
159+Or, the schema browser can be integrated into the backend itself. This is done via
160+`servant-swagger-ui` package, which embeds `swagger-ui` into the Servant backend.
161+
162+First, define a sub-api that will serve the documentation:
163+
164+```haskell
165+type DocsAPI = SwaggerSchemaUI "swagger-ui" "swagger.json"
166+```
167+
168+And a full API for your backend, which combines your endpoints and `DocsAPI`:
169+
170+```haskell
171+type API = DocsAPI :<|> TodoAPI
172+```
173+
174+`SwaggerSchemaUI` describes an API that will serve the interactive schema browser at `/swagger-ui`
175+of your server and the specification in JSON format at `/swagger.json`. Of course, both paths are
176+customizable.
177+
178+A handler for `SwaggerSchemaUI`, called `swaggerSchemaUIServer`, expectes one argument: the
179+specification itself. In our case, it's `todoOpenApi`.
180+
181+```haskell
182+todoServer :: Servant.Server API
183+todoServer = swaggerSchemaUIServer todoOpenApi
184+ :<|> error "The actual TODO API is not implemented"
185+```
186+
187+Now the server can be run as usual:
188+
189+```haskell
190+main :: IO ()
191+main = do
192+ Warp.run 5000 $ serve (Proxy :: Proxy API) todoServer
193+```
194+
195+Run this example, navigate to http://localhost:5000/swagger-ui and you will see the interactive
196+schema browser:
197+
198+![](./swagger-ui-example.png)
199+
200+You can make requests in this UI and they will be sent to your backend as expected.
doc/cookbook/openapi3/openapi3.cabalnew file+26
@@ -0,0 +1,26 @@
1+name: cookbook-openapi3
2+version: 2.1
3+synopsis: OpenAPI 3.0 schema generation example
4+homepage: http://docs.servant.dev/
5+license: BSD3
6+license-file: ../../../servant/LICENSE
7+author: Servant Contributors
8+maintainer: haskell-servant-maintainers@googlegroups.com
9+build-type: Simple
10+cabal-version: >=1.10
11+tested-with: GHC==8.6.5, GHC==8.8.3, GHC ==8.10.7
12+
13+executable cookbook-openapi3
14+ main-is: OpenAPI.lhs
15+ build-tool-depends: markdown-unlit:markdown-unlit
16+ default-language: Haskell2010
17+ ghc-options: -Wall -pgmL markdown-unlit
18+ build-depends: base >= 4.9 && <5
19+ , aeson
20+ , openapi3
21+ , servant
22+ , servant-server
23+ , servant-openapi3
24+ , servant-swagger-ui
25+ , text
26+ , warp
doc/cookbook/openapi3/swagger-ui-example.pngnew file

Binary file not shown.