Skip to content
This repository was archived by the owner on Oct 19, 2024. It is now read-only.

Commit f40b2d7

Browse files
authored
Expose Swagger definition + documentation (#231)
1 parent a7b2e2e commit f40b2d7

File tree

9 files changed

+298
-161
lines changed

9 files changed

+298
-161
lines changed

docs/_data/sidebar.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ options:
2222
- title: Mu-Optics
2323
url: optics/
2424

25-
- title: RPC services
25+
- title: Services
2626
url: rpc/
2727
nested_options:
2828
- title: gRPC servers
@@ -31,8 +31,11 @@ options:
3131
- title: gRPC clients
3232
url: grpc/client/
3333

34-
- title: GraphQL services
35-
url: graphql/
34+
- title: GraphQL
35+
url: graphql/
36+
37+
- title: OpenAPI / REST
38+
url: openapi/
3639

3740
- title: Integrations
3841
nested_options:

docs/docs/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ Mu-Haskell is a set of packages that help you build both servers and clients for
1515
* [Serialization formats]({% link docs/serializers.md %}): Protocol Buffers and Avro
1616
* [Registry]({% link docs/registry.md %})
1717
* [Optics]({% link docs/optics.md %})
18-
* [RPC services]({% link docs/rpc.md %})
18+
* [Services]({% link docs/rpc.md %})
1919
* [gRPC server]({% link docs/grpc-server.md %})
2020
* [gRPC client]({% link docs/grpc-client.md %})
21-
* [GraphQL services]({% link docs/graphql.md %})
21+
* [GraphQL]({% link docs/graphql.md %})
22+
* [OpenAPI / REST]({% link docs/rest.md %})
2223
* Integration with other libraries
2324
* [Databases]({% link docs/db.md %}), including resource pools
2425
* [Using transformers]({% link docs/transformer.md %}): look here for logging

docs/docs/rest.md

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
---
2+
layout: docs
3+
title: OpenAPI / REST services
4+
permalink: openapi/
5+
---
6+
7+
# OpenAPI / REST services
8+
9+
In order to expose a Mu server using a OpenAPI or REST interface, we make use of the awesome [Servant](https://docs.servant.dev/en/stable/) library. Both libraries describe the API of a server at the type level, use the notion of _handlers_, and follow a similar structure.
10+
11+
The `mu-servant-server` package contains a function `servantServerHandlers` which unpacks the Mu handlers and repackages them as Servant handlers, with some minor changes to support streaming. The trickiest part, however, is translating the Mu server _type_ into a Servant server _type_.
12+
13+
## Annotating the server
14+
15+
When Mu methods are converted to Servant APIs, you may customize certain aspects of the resulting API, including the route, HTTP method, and HTTP status. Additionally, you must specify which content types use be used when encoding and decoding each type in your schema that appears in your methods. All of this customization is done with annotations, via the `AnnotatedSchema` and `AnnotatedPackage` type families.
16+
17+
For the server we have developed in the [generic RPC section]({% link docs/rpc.md %}), the instances for the services look as follows:
18+
19+
```haskell
20+
type instance AnnotatedPackage ServantRoute QuickstartService
21+
= '[ 'AnnService "Greeter" ('ServantTopLevelRoute '["greet"])
22+
, 'AnnMethod "Greeter" "SayHello"
23+
('ServantRoute '["say", "hello"] 'POST 200),
24+
]
25+
```
26+
27+
The first annotation defines that the whole service lives in the `/greet` route. Each method then gets its own route and HTTP verb. To execute `SayHello`, one has to make a `POST` request to `/greet/say/hello`. The last element is the HTTP status code to be returned by default, in this case `200` which means success.
28+
29+
You also need to define how message types can be serialized in the API. This will be translated to a `ReqBody` in the corresponding Servant API, which requires a list of acceptable content types for the request. We provide a `DefaultServantContentTypes` which uses JSON for both unary and streaming calls.
30+
31+
```haskell
32+
type instance
33+
AnnotatedSchema ServantContentTypes QuickstartSchema =
34+
'[ 'AnnType "HelloRequest" DefaultServantContentTypes,
35+
'AnnType "HelloResponse" DefaultServantContentTypes
36+
]
37+
```
38+
39+
The `MimeRender`/`MimeUnrender` instances necessary to perform this encoding/decoding must exist for the Haskell type you use to represent messages. In this case, that means that both types must support conversion to JSON, which can be achieved using `mu-schema` in combination with `DerivingVia`.
40+
41+
```haskell
42+
{-# language DerivingVia #-}
43+
44+
import qualified Data.Aeson as J
45+
import Mu.Adapter.Json ()
46+
47+
newtype HelloRequest = HelloRequest { name :: T.Text }
48+
deriving ( Show, Eq, Generic
49+
, ToSchema QuickstartSchema "HelloRequest"
50+
, FromSchema QuickstartSchema "HelloRequest" )
51+
deriving (J.ToJSON, J.FromJSON)
52+
via (WithSchema QuickstartSchema "HelloRequest" HelloRequest)
53+
```
54+
55+
56+
If you forget to provide one of these required instances, you will see a message like the following:
57+
58+
```
59+
• Missing required AnnotatedPackage ServantRoute type instance
60+
for "myschema" package
61+
• When checking the inferred type
62+
```
63+
64+
followed by a large and difficult to read type representing several stuck type families. This message is an indication that you must provide an `AnnotatedPackage` type instance, with a domain of `ServantRoute` for the package with the name `myschema`.
65+
66+
## Exposing the server
67+
68+
You are now ready to expose your server using Servant!
69+
70+
```haskell
71+
import Mu.Servant.Server
72+
import Servant.Server
73+
74+
main =
75+
let api = packageAPI (quickstartServer @ServerErrorIO)
76+
server = servantServerHandlers toHandler quickstartServer
77+
in run 8081 (serve api server)
78+
```
79+
80+
The last line uses functions from Servant and Warp to run the server. The `serve` function has two parameters:
81+
- One is the definition of the API, which can be obtained using the provided `packageAPI` with your server. In this case we had to make explicit the monad we are operating to avoid an ambiguity error.
82+
- The other is the set of Servant handlers, which can be obtained by using `servantServerHandlers toHandler`.
83+
84+
## Integration with Swagger UI
85+
86+
You can easily expose not only the server itself, but also its [Swagger / OpenAPI](https://swagger.io/) schema easily, alongside a [Swagger UI](https://swagger.io/tools/swagger-ui/) for testing purposes. Here we make use of the awesome [`servant-swagger-ui` package](https://github.com/haskell-servant/servant-swagger-ui).
87+
88+
First of all, you need to specify that you want an additional component in your Servant API. You do so in the annotation:
89+
90+
```haskell
91+
type instance AnnotatedPackage ServantRoute QuickstartService
92+
= '[ 'AnnPackage ('ServantAdditional (SwaggerSchemaUI "swagger-ui" "swagger.json"))
93+
, {- rest of annotations -} ]
94+
```
95+
96+
The implementation of this additional component is given by using `servantServerHandlersExtra`, instead of its "non-extra" version. The aforementioned package is ready for consumption in that way:
97+
98+
```haskell
99+
import Mu.Servant.Server
100+
import Servant.Server
101+
import Servant.Swagger.UI
102+
103+
main =
104+
let svc = quickstartServer @ServerErrorIO
105+
api = packageAPI svc
106+
server = servantServerHandlersExtra
107+
(swaggerSchemaUIServer (swagger svc))
108+
toHandler svc
109+
in run 8081 (serve api server)
110+
```
111+
112+
And that's all! When you users surf to `yourserver/swagger-ui` they'll see a color- and featureful explanation of the endpoints of your server.
113+
114+
## Type translation
115+
116+
> This is not required for using `mu-servant-server`, but may help you understanding how it works under the hood and diagnosing problems.
117+
118+
There are essentially four categories of `Method` types and each of these is translated slightly differently.
119+
120+
### Full unary
121+
122+
Full unary methods have non-streaming arguments and a non-streaming response. Most HTTP endpoints expect unary requests and return unary responses. Unary method handlers look like this
123+
124+
```haskell
125+
(MonadServer m) => requestType -> m responseType
126+
```
127+
128+
For a handler like this, the corresponding "Servant" API type would be
129+
130+
```haskell
131+
type MyUnaryAPI =
132+
route :>
133+
ReqBody ctypes1 requestType :>
134+
Verb method status ctypes2 responseType
135+
```
136+
137+
As you can see, the request body contains a `requestType` value, and the response body contains a `responseType` value. All other types are derived from Mu annotations.
138+
139+
### Server streaming
140+
141+
Server streaming methods have non-streaming arguments, but the response is streamed back to the client. Server stream handlers look like this
142+
143+
```haskell
144+
(MonadServer m) => requestType -> ConduitT responseType Void m () -> m ()
145+
```
146+
147+
For a handler like this, the corresponding Servant API type would be
148+
149+
```haskell
150+
type MyServerStreamAPI =
151+
route :>
152+
ReqBody ctypes requestType :>
153+
Stream method status framing ctype (SourceIO (StreamResult responseType))
154+
```
155+
156+
The request body contains a `requestType` value. The response body is a stream of `StreamResult` responseType@ values. `StreamResult responseType` contains either a `responseType` value or an error message describing a problem that occurred while producing `responseType` values. All other types are derived from Mu annotations.
157+
158+
### Client streaming
159+
160+
Client streaming methods have a streaming argument, but the response is unary. Client stream handlers look like this
161+
162+
```haskell
163+
(MonadServer m) => ConduitT () requestType m () -> m responseType
164+
```
165+
166+
For a handler like this, the corresponding Servant API type would be
167+
168+
```haskell
169+
type MyClientStreamAPI =
170+
route :>
171+
StreamBody framing ctype (SourceIO requestType) :>
172+
Verb method status ctypes responseType
173+
```
174+
175+
### Bidirectional streaming
176+
177+
Bidirectional streaming method have a streaming argument and a streaming response. Bidirectional stream handlers look like this
178+
179+
```haskell
180+
> (MonadServer m) => ConduitT () requestType m () -> ConduitT responseType Void m () -> m()
181+
```
182+
183+
For a handler like this, the corresponding Servant API type would be
184+
185+
```haskell
186+
type MyBidirectionalStreamAPI =
187+
StreamBody framing1 ctype1 (SourceIO requestType) :>
188+
Stream method status framing2 ctype2 (SourceIO (StreamResult responseType))
189+
```
190+
191+
This type should look familiar if you already looked at the server streaming and client streaming examples. The request body is a stream of `requestType` values, and the response body is a stream of `StreamResult responseType` values. All the other types involved are derived from Mu annotations.
192+

examples/health-check/mu-example-health-check.cabal

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,11 @@ executable health-server
3636
, mu-tracing >=0.4.0
3737
, prometheus-client >= 1 && <2
3838
, servant-server
39+
, servant-swagger-ui
3940
, stm >=2.5 && <3
4041
, stm-conduit >=4 && <5
4142
, stm-containers >=1.1 && <2
43+
, swagger2
4244
, text >=1.2 && <2
4345
, tracing-control >=0.0.6
4446
, wai >=3.2 && <4
@@ -59,6 +61,7 @@ executable health-client-tyapps
5961
, mu-protobuf >=0.4.0
6062
, mu-rpc >=0.4.0
6163
, mu-schema >=0.3.0
64+
, swagger2
6265
, text >=1.2 && <2
6366

6467
hs-source-dirs: src
@@ -76,6 +79,7 @@ executable health-client-record
7679
, mu-protobuf >=0.4.0
7780
, mu-rpc >=0.4.0
7881
, mu-schema >=0.3.0
82+
, swagger2
7983
, text >=1.2 && <2
8084

8185
hs-source-dirs: src

examples/health-check/src/Definition.hs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
module Definition where
1616

1717
import qualified Data.Aeson as J
18+
import qualified Data.Swagger as Swagger
1819
import Data.Text as T
1920
import GHC.Generics
2021

@@ -34,27 +35,31 @@ newtype HealthCheckMsg
3435
= HealthCheckMsg { nameService :: T.Text }
3536
deriving ( Eq, Show, Ord, Generic
3637
, ToSchema HealthCheckSchema "HealthCheck"
37-
, FromSchema HealthCheckSchema "HealthCheck" )
38+
, FromSchema HealthCheckSchema "HealthCheck"
39+
, Swagger.ToSchema )
3840
deriving (J.ToJSON, J.FromJSON)
3941
via (WithSchema HealthCheckSchema "HealthCheck" HealthCheckMsg)
4042
newtype ServerStatusMsg
4143
= ServerStatusMsg { status :: T.Text }
4244
deriving ( Eq, Show, Ord, Generic
4345
, ToSchema HealthCheckSchema "ServerStatus"
44-
, FromSchema HealthCheckSchema "ServerStatus" )
46+
, FromSchema HealthCheckSchema "ServerStatus"
47+
, Swagger.ToSchema )
4548
deriving (J.ToJSON, J.FromJSON)
4649
via (WithSchema HealthCheckSchema "ServerStatus" ServerStatusMsg)
4750
data HealthStatusMsg
4851
= HealthStatusMsg { hc :: Maybe HealthCheckMsg, status :: Maybe ServerStatusMsg }
4952
deriving ( Eq, Show, Ord, Generic
5053
, ToSchema HealthCheckSchema "HealthStatus"
51-
, FromSchema HealthCheckSchema "HealthStatus" )
54+
, FromSchema HealthCheckSchema "HealthStatus"
55+
, Swagger.ToSchema )
5256
deriving (J.ToJSON, J.FromJSON)
5357
via (WithSchema HealthCheckSchema "HealthStatus" HealthStatusMsg)
5458
newtype AllStatusMsg
5559
= AllStatusMsg { all :: [HealthStatusMsg] }
5660
deriving ( Eq, Show, Ord, Generic
5761
, ToSchema HealthCheckSchema "AllStatus"
58-
, FromSchema HealthCheckSchema "AllStatus" )
62+
, FromSchema HealthCheckSchema "AllStatus"
63+
, Swagger.ToSchema )
5964
deriving (J.ToJSON, J.FromJSON)
6065
via (WithSchema HealthCheckSchema "AllStatus" AllStatusMsg)

examples/health-check/src/Server.hs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import Monitor.Tracing.Zipkin (Endpoint (..))
2727
import Network.Wai.Handler.Warp
2828
import Prometheus
2929
import Servant.Server (serve)
30+
import Servant.Swagger.UI
3031
import qualified StmContainers.Map as M
3132

3233
import Mu.GraphQL.Server
@@ -54,11 +55,13 @@ main = do
5455
upd <- newTBMChanIO 100
5556
-- Put together the server
5657
let s = zipkin rootInfo $ prometheus met $ server m upd
58+
servantAPI = packageAPI s
59+
servant = servantServerHandlersExtra (toHandler . runZipkin zpk)
60+
(swaggerSchemaUIServer (swagger s)) s
5761
-- Run the app
5862
putStrLn "running health check application"
5963
runConcurrently $ (\_ _ _ _ -> ())
60-
<$> Concurrently (runner 8080
61-
(serve (packageAPI s) (servantServerHandlers (toHandler . runZipkin zpk) s)))
64+
<$> Concurrently (runner 8080 (serve servantAPI servant))
6265
<*> Concurrently (runner 50051 (gRpcAppTrans msgProtoBuf (runZipkin zpk) s))
6366
<*> Concurrently (runner 50052 (gRpcAppTrans msgAvro (runZipkin zpk) s))
6467
<*> Concurrently (runner 50053 (graphQLAppTransQuery (runZipkin zpk) s
@@ -151,7 +154,8 @@ instance MonadMonitor m => MonadMonitor (TraceT m)
151154
-- Information for servant
152155

153156
type instance AnnotatedPackage ServantRoute HealthCheckServiceFS2
154-
= '[ 'AnnService "HealthCheckServiceFS2"
157+
= '[ 'AnnPackage ('ServantAdditional (SwaggerSchemaUI "swagger-ui" "swagger.json"))
158+
, 'AnnService "HealthCheckServiceFS2"
155159
('ServantTopLevelRoute '["health"])
156160
, 'AnnMethod "HealthCheckServiceFS2" "setStatus"
157161
('ServantRoute '["status"] 'POST 200)

generate-haddock-docs.sh

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ stack exec --no-ghc-package-path standalone-haddock -- -o ${DOCSDIR} \
1515
--package-db=$(stack path --snapshot-pkg-db) \
1616
--package-db=$(stack path --local-pkg-db) \
1717
--hyperlink-source \
18-
core/schema core/rpc core/optics \
18+
core/schema core/rpc core/optics core/lens \
1919
adapter/avro adapter/protobuf adapter/persistent adapter/kafka \
20-
grpc/common grpc/client grpc/server graphql
20+
instrumentation/prometheus instrumentation/tracing \
21+
grpc/common grpc/client grpc/server graphql servant/server
2122

2223
echo "Setting Linuwial theme on Haddock generated docs"
2324
if [ "$1" == "ocean" ]

servant/server/mu-servant-server.cabal

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ library
3434
, mu-schema
3535
, servant
3636
, servant-server
37+
, servant-swagger
3738
, stm
39+
, swagger2
3840
, utf8-string
3941

4042
hs-source-dirs: src

0 commit comments

Comments
 (0)