Skip to content

Commit 2ea7d39

Browse files
committed
Merge branch 'support-webhooks' into pre-release-5-0-0
2 parents e3e1242 + 9811ac7 commit 2ea7d39

File tree

12 files changed

+228
-56
lines changed

12 files changed

+228
-56
lines changed

examples/src/main/kotlin/io/github/smiley4/ktoropenapi/examples/TypesafeRouting.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ private fun Application.myModule() {
5151

5252
routing {
5353

54-
// add the routes for the api-spec, swagger-ui and redoc
54+
// add the routes for the api-spec, swagger-ui and redoc
5555
route("api.json") {
5656
openApi()
5757
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package io.github.smiley4.ktoropenapi.examples
2+
3+
import io.github.smiley4.ktoropenapi.OpenApi
4+
import io.github.smiley4.ktoropenapi.openApi
5+
import io.github.smiley4.ktoropenapi.post
6+
import io.github.smiley4.ktoropenapi.webhook
7+
import io.github.smiley4.ktorredoc.redoc
8+
import io.github.smiley4.ktorswaggerui.swaggerUI
9+
import io.ktor.http.ContentType
10+
import io.ktor.http.HttpMethod
11+
import io.ktor.http.HttpStatusCode
12+
import io.ktor.server.application.Application
13+
import io.ktor.server.application.install
14+
import io.ktor.server.engine.embeddedServer
15+
import io.ktor.server.netty.Netty
16+
import io.ktor.server.response.respond
17+
import io.ktor.server.routing.route
18+
import io.ktor.server.routing.routing
19+
20+
fun main() {
21+
embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true)
22+
}
23+
24+
private fun Application.myModule() {
25+
26+
// Install the "OpenApi"-Plugin
27+
install(OpenApi)
28+
29+
routing {
30+
31+
// add the routes for the api-spec, swagger-ui and redoc
32+
route("api.json") {
33+
openApi()
34+
}
35+
route("swagger") {
36+
swaggerUI("/api.json")
37+
}
38+
route("redoc") {
39+
redoc("/api.json")
40+
}
41+
42+
// a "normal" documented route to register for notifications
43+
post("registerForAlert", {
44+
description = "Register an URL to be called when new concerts are scheduled."
45+
request {
46+
body<String> {
47+
description = "The URL to be notified about approaching concerts."
48+
}
49+
}
50+
}) {
51+
call.respond(HttpStatusCode.NotImplemented, Unit)
52+
}
53+
54+
// documentation of the webhook to notify
55+
webhook(HttpMethod.Post, "concertAlert") {
56+
description = "Notify the registered URL with details of an upcoming concert"
57+
request {
58+
body<String> {
59+
mediaTypes(ContentType.Text.Plain)
60+
required = true
61+
}
62+
}
63+
}
64+
65+
}
66+
67+
}

ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/DocumentedRouteSelector.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,3 +301,14 @@ fun Route.head(
301301
): Route {
302302
return documentation(builder) { head(body) }
303303
}
304+
305+
306+
//===============================//
307+
// WEBHOOK //
308+
//===============================//
309+
310+
internal val webhooks = mutableMapOf<String, Pair<HttpMethod, RouteConfig>>()
311+
312+
fun webhook(method: HttpMethod, name: String, builder: RouteConfig.() -> Unit = { },) {
313+
webhooks[name] = method to RouteConfig().apply(builder)
314+
}

ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/OpenApiPlugin.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ package io.github.smiley4.ktoropenapi
33
import io.github.oshai.kotlinlogging.KotlinLogging
44
import io.github.smiley4.ktoropenapi.builder.OpenApiSpecBuilder
55
import io.github.smiley4.ktoropenapi.builder.route.RouteCollector
6+
import io.github.smiley4.ktoropenapi.builder.route.RouteMeta
67
import io.github.smiley4.ktoropenapi.config.OpenApiPluginConfig
78
import io.github.smiley4.ktoropenapi.config.OutputFormat
89
import io.github.smiley4.ktoropenapi.data.OpenApiPluginData
910
import io.ktor.http.ContentType
11+
import io.ktor.http.HttpMethod
1012
import io.ktor.http.HttpStatusCode
1113
import io.ktor.server.application.Application
1214
import io.ktor.server.application.ApplicationPlugin
@@ -51,7 +53,15 @@ object OpenApiPlugin {
5153
* Generates new openapi
5254
*/
5355
fun generateOpenApiSpecs(application: Application) {
54-
val routes = RouteCollector().collect({ application.plugin(RoutingRoot) }, config)
56+
val routes = RouteCollector().collect({ application.plugin(RoutingRoot) }, config) + webhooks.map { (name, entry) ->
57+
RouteMeta(
58+
method = entry.first,
59+
path = name,
60+
documentation = entry.second.build(),
61+
protected = false,
62+
isWebhook = true
63+
)
64+
}
5565
val specs = OpenApiSpecBuilder().build(config, routes)
5666
openApiSpecs.clear()
5767
openApiSpecs.putAll(specs)

ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/OpenApiSpecBuilder.kt

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.github.smiley4.ktoropenapi.builder
22

33
import io.github.oshai.kotlinlogging.KotlinLogging
4+
import io.github.smiley4.ktoropenapi.OpenApiPlugin.config
45
import io.github.smiley4.ktoropenapi.builder.example.ExampleContext
56
import io.github.smiley4.ktoropenapi.builder.example.ExampleContextImpl
67
import io.github.smiley4.ktoropenapi.builder.openapi.ComponentsBuilder
@@ -25,6 +26,7 @@ import io.github.smiley4.ktoropenapi.builder.openapi.SecuritySchemesBuilder
2526
import io.github.smiley4.ktoropenapi.builder.openapi.ServerBuilder
2627
import io.github.smiley4.ktoropenapi.builder.openapi.TagBuilder
2728
import io.github.smiley4.ktoropenapi.builder.openapi.TagExternalDocumentationBuilder
29+
import io.github.smiley4.ktoropenapi.builder.openapi.WebhooksBuilder
2830
import io.github.smiley4.ktoropenapi.builder.route.RouteMeta
2931
import io.github.smiley4.ktoropenapi.builder.schema.SchemaContext
3032
import io.github.smiley4.ktoropenapi.builder.schema.SchemaContextImpl
@@ -88,6 +90,36 @@ internal class OpenApiSpecBuilder {
8890
schemaContext: SchemaContext,
8991
exampleContext: ExampleContext,
9092
): OpenApiBuilder {
93+
val pathBuilder = PathBuilder(
94+
operationBuilder = OperationBuilder(
95+
operationTagsBuilder = OperationTagsBuilder(config),
96+
parameterBuilder = ParameterBuilder(
97+
schemaContext = schemaContext,
98+
exampleContext = exampleContext,
99+
),
100+
requestBodyBuilder = RequestBodyBuilder(
101+
contentBuilder = ContentBuilder(
102+
schemaContext = schemaContext,
103+
exampleContext = exampleContext,
104+
headerBuilder = HeaderBuilder(schemaContext)
105+
)
106+
),
107+
responsesBuilder = ResponsesBuilder(
108+
responseBuilder = ResponseBuilder(
109+
headerBuilder = HeaderBuilder(schemaContext),
110+
contentBuilder = ContentBuilder(
111+
schemaContext = schemaContext,
112+
exampleContext = exampleContext,
113+
headerBuilder = HeaderBuilder(schemaContext)
114+
)
115+
),
116+
config = config
117+
),
118+
securityRequirementsBuilder = SecurityRequirementsBuilder(config),
119+
externalDocumentationBuilder = ExternalDocumentationBuilder(),
120+
serverBuilder = ServerBuilder()
121+
)
122+
)
91123
return OpenApiBuilder(
92124
config = config,
93125
schemaContext = schemaContext,
@@ -103,36 +135,10 @@ internal class OpenApiSpecBuilder {
103135
),
104136
pathsBuilder = PathsBuilder(
105137
config = config,
106-
pathBuilder = PathBuilder(
107-
operationBuilder = OperationBuilder(
108-
operationTagsBuilder = OperationTagsBuilder(config),
109-
parameterBuilder = ParameterBuilder(
110-
schemaContext = schemaContext,
111-
exampleContext = exampleContext,
112-
),
113-
requestBodyBuilder = RequestBodyBuilder(
114-
contentBuilder = ContentBuilder(
115-
schemaContext = schemaContext,
116-
exampleContext = exampleContext,
117-
headerBuilder = HeaderBuilder(schemaContext)
118-
)
119-
),
120-
responsesBuilder = ResponsesBuilder(
121-
responseBuilder = ResponseBuilder(
122-
headerBuilder = HeaderBuilder(schemaContext),
123-
contentBuilder = ContentBuilder(
124-
schemaContext = schemaContext,
125-
exampleContext = exampleContext,
126-
headerBuilder = HeaderBuilder(schemaContext)
127-
)
128-
),
129-
config = config
130-
),
131-
securityRequirementsBuilder = SecurityRequirementsBuilder(config),
132-
externalDocumentationBuilder = ExternalDocumentationBuilder(),
133-
serverBuilder = ServerBuilder()
134-
)
135-
)
138+
pathBuilder = pathBuilder
139+
),
140+
webhooksBuilder = WebhooksBuilder(
141+
pathBuilder = pathBuilder
136142
),
137143
componentsBuilder = ComponentsBuilder(
138144
config = config,

ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/openapi/OpenApiBuilder.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ internal class OpenApiBuilder(
2020
private val serverBuilder: ServerBuilder,
2121
private val tagBuilder: TagBuilder,
2222
private val pathsBuilder: PathsBuilder,
23+
private val webhooksBuilder: WebhooksBuilder,
2324
private val componentsBuilder: ComponentsBuilder,
2425
) {
2526

@@ -31,7 +32,8 @@ internal class OpenApiBuilder(
3132
it.externalDocs = externalDocumentationBuilder.build(config.externalDocs)
3233
it.servers = config.servers.map { server -> serverBuilder.build(server) }
3334
it.tags = config.tagsConfig.tags.map { tag -> tagBuilder.build(tag) }
34-
it.paths = pathsBuilder.build(routes)
35+
it.paths = pathsBuilder.build(routes.filter { r -> !r.isWebhook})
36+
it.webhooks = webhooksBuilder.build(routes.filter { r -> r.isWebhook})
3537
it.components = componentsBuilder.build(schemaContext.getComponentSection(), exampleContext.getComponentSection())
3638
}
3739
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package io.github.smiley4.ktoropenapi.builder.openapi
2+
3+
import io.github.smiley4.ktoropenapi.builder.route.RouteMeta
4+
import io.swagger.v3.oas.models.PathItem
5+
6+
/**
7+
* Build the openapi "webhooks" section.
8+
* See [OpenAPI Specification - Webhooks](https://spec.openapis.org/oas/v3.1.0.html#oasWebhooks)
9+
*/
10+
internal class WebhooksBuilder(
11+
private val pathBuilder: PathBuilder
12+
) {
13+
14+
fun build(routes: Collection<RouteMeta>): Map<String, PathItem> {
15+
return routes.associate { route ->
16+
route.path to pathBuilder.build(route)
17+
}
18+
}
19+
20+
}

ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/route/RouteCollector.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ internal class RouteCollector {
4040
method = getMethod(route),
4141
path = getPath(route, config),
4242
documentation = documentation.build(),
43-
protected = documentation.protected ?: isProtected(route)
43+
protected = documentation.protected ?: isProtected(route),
44+
isWebhook = false
4445
)
4546
}
4647
.filter { !it.documentation.hidden }

ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/route/RouteMeta.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ internal data class RouteMeta(
1010
val path: String,
1111
val method: HttpMethod,
1212
val documentation: RouteData,
13-
val protected: Boolean
13+
val protected: Boolean,
14+
val isWebhook: Boolean,
1415
)

ktor-openapi/src/test/kotlin/io/github/smiley4/ktorswaggerui/builder/OpenApiBuilderTest.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import io.github.smiley4.ktoropenapi.builder.openapi.SecuritySchemesBuilder
2424
import io.github.smiley4.ktoropenapi.builder.openapi.ServerBuilder
2525
import io.github.smiley4.ktoropenapi.builder.openapi.TagBuilder
2626
import io.github.smiley4.ktoropenapi.builder.openapi.TagExternalDocumentationBuilder
27+
import io.github.smiley4.ktoropenapi.builder.openapi.WebhooksBuilder
2728
import io.github.smiley4.ktoropenapi.builder.route.RouteMeta
2829
import io.github.smiley4.ktoropenapi.builder.schema.SchemaContext
2930
import io.github.smiley4.ktoropenapi.builder.schema.SchemaContextImpl
@@ -172,6 +173,38 @@ class OpenApiBuilderTest : StringSpec({
172173
)
173174
)
174175
),
176+
webhooksBuilder = WebhooksBuilder(
177+
pathBuilder = PathBuilder(
178+
operationBuilder = OperationBuilder(
179+
operationTagsBuilder = OperationTagsBuilder(pluginConfigData),
180+
parameterBuilder = ParameterBuilder(
181+
schemaContext = schemaContext,
182+
exampleContext = exampleContext
183+
),
184+
requestBodyBuilder = RequestBodyBuilder(
185+
contentBuilder = ContentBuilder(
186+
schemaContext = schemaContext,
187+
exampleContext = exampleContext,
188+
headerBuilder = HeaderBuilder(schemaContext)
189+
)
190+
),
191+
responsesBuilder = ResponsesBuilder(
192+
responseBuilder = ResponseBuilder(
193+
headerBuilder = HeaderBuilder(schemaContext),
194+
contentBuilder = ContentBuilder(
195+
schemaContext = schemaContext,
196+
exampleContext = exampleContext,
197+
headerBuilder = HeaderBuilder(schemaContext)
198+
)
199+
),
200+
config = pluginConfigData
201+
),
202+
securityRequirementsBuilder = SecurityRequirementsBuilder(pluginConfigData),
203+
externalDocumentationBuilder = ExternalDocumentationBuilder(),
204+
serverBuilder = ServerBuilder()
205+
)
206+
)
207+
),
175208
componentsBuilder = ComponentsBuilder(
176209
config = pluginConfigData,
177210
securitySchemesBuilder = SecuritySchemesBuilder(

0 commit comments

Comments
 (0)