Skip to content

Commit 5f5c8c9

Browse files
authored
Merge pull request #219 from Shun-Arahata/shun/support-x-tagGroups
support x-tagGroups
2 parents 84966d6 + 665656d commit 5f5c8c9

File tree

7 files changed

+325
-1
lines changed

7 files changed

+325
-1
lines changed
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
package io.github.smiley4.ktoropenapi.examples
2+
3+
import io.github.smiley4.ktoropenapi.OpenApi
4+
import io.github.smiley4.ktoropenapi.get
5+
import io.github.smiley4.ktoropenapi.openApi
6+
import io.github.smiley4.ktoropenapi.post
7+
import io.github.smiley4.ktorredoc.redoc
8+
import io.github.smiley4.ktorswaggerui.swaggerUI
9+
import io.ktor.http.HttpStatusCode
10+
import io.ktor.server.application.Application
11+
import io.ktor.server.application.install
12+
import io.ktor.server.engine.embeddedServer
13+
import io.ktor.server.netty.Netty
14+
import io.ktor.server.response.respondText
15+
import io.ktor.server.routing.route
16+
import io.ktor.server.routing.routing
17+
18+
/**
19+
* Example demonstrating the x-tagGroups OpenAPI vendor extension.
20+
* Tag groups organize tags into logical sections in the documentation UI (e.g., Redoc).
21+
*
22+
* IMPORTANT: All tags used in your operations should be included in a tag group,
23+
* as tags not in any group may not be displayed in the documentation.
24+
*
25+
* Run the application and navigate to:
26+
* - Redoc: http://localhost:8080/redoc (recommended - has best x-tagGroups support)
27+
* - Swagger UI: http://localhost:8080/swagger
28+
* - OpenAPI Spec: http://localhost:8080/api.json
29+
*/
30+
fun main() {
31+
embeddedServer(Netty, port = 8080, host = "localhost", module = Application::tagGroupsExample).start(wait = true)
32+
}
33+
34+
private fun Application.tagGroupsExample() {
35+
36+
// Install and configure the OpenApi Plugin with tag groups
37+
install(OpenApi) {
38+
info {
39+
title = "Tag Groups Example API"
40+
version = "1.0.0"
41+
description = "Example API demonstrating x-tagGroups for organizing tags in documentation"
42+
}
43+
44+
// Configure tags with descriptions
45+
tags {
46+
tag("Users") {
47+
description = "User management operations"
48+
}
49+
tag("API Keys") {
50+
description = "API key management operations"
51+
}
52+
tag("Admin") {
53+
description = "Administrative operations"
54+
}
55+
tag("Orders") {
56+
description = "Order processing operations"
57+
}
58+
tag("Products") {
59+
description = "Product catalog operations"
60+
}
61+
tag("Analytics") {
62+
description = "Analytics and reporting"
63+
}
64+
65+
// Define tag groups using the x-tagGroups extension
66+
// This organizes tags into logical groups in the documentation sidebar
67+
tagGroup("User Management") {
68+
tag("Users")
69+
tag("API Keys")
70+
tag("Admin")
71+
}
72+
73+
tagGroup("E-Commerce") {
74+
tag("Orders")
75+
tag("Products")
76+
}
77+
78+
tagGroup("Monitoring") {
79+
tag("Analytics")
80+
}
81+
}
82+
}
83+
84+
routing {
85+
86+
route("api.json") {
87+
openApi()
88+
}
89+
90+
route("swagger") {
91+
swaggerUI("/api.json")
92+
}
93+
94+
route("redoc") {
95+
redoc("/api.json")
96+
}
97+
98+
// User Management endpoints
99+
get("users", {
100+
tags = listOf("Users")
101+
description = "List all users"
102+
response {
103+
HttpStatusCode.OK to {
104+
description = "List of users"
105+
}
106+
}
107+
}) {
108+
call.respondText("[]")
109+
}
110+
111+
post("users", {
112+
tags = listOf("Users")
113+
description = "Create a new user"
114+
response {
115+
HttpStatusCode.Created to {
116+
description = "User created successfully"
117+
}
118+
}
119+
}) {
120+
call.respondText("User created", status = HttpStatusCode.Created)
121+
}
122+
123+
get("api-keys", {
124+
tags = listOf("API Keys")
125+
description = "List API keys"
126+
response {
127+
HttpStatusCode.OK to {
128+
description = "List of API keys"
129+
}
130+
}
131+
}) {
132+
call.respondText("[]")
133+
}
134+
135+
post("admin/settings", {
136+
tags = listOf("Admin")
137+
description = "Update admin settings"
138+
response {
139+
HttpStatusCode.OK to {
140+
description = "Settings updated"
141+
}
142+
}
143+
}) {
144+
call.respondText("Settings updated")
145+
}
146+
147+
// E-Commerce endpoints
148+
get("orders", {
149+
tags = listOf("Orders")
150+
description = "List all orders"
151+
response {
152+
HttpStatusCode.OK to {
153+
description = "List of orders"
154+
}
155+
}
156+
}) {
157+
call.respondText("[]")
158+
}
159+
160+
get("products", {
161+
tags = listOf("Products")
162+
description = "List all products"
163+
response {
164+
HttpStatusCode.OK to {
165+
description = "List of products"
166+
}
167+
}
168+
}) {
169+
call.respondText("[]")
170+
}
171+
172+
// Monitoring endpoints
173+
get("analytics/stats", {
174+
tags = listOf("Analytics")
175+
description = "Get analytics statistics"
176+
response {
177+
HttpStatusCode.OK to {
178+
description = "Analytics data"
179+
}
180+
}
181+
}) {
182+
call.respondText("{}")
183+
}
184+
185+
}
186+
187+
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ internal class OpenApiBuilder(
3535
it.paths = pathsBuilder.build(routes.filter { r -> !r.isWebhook})
3636
it.webhooks = webhooksBuilder.build(routes.filter { r -> r.isWebhook})
3737
it.components = componentsBuilder.build(schemaContext.getComponentSection(), exampleContext.getComponentSection())
38+
39+
// Add x-tagGroups vendor extension if tag groups are configured
40+
if (config.tagsConfig.tagGroups.isNotEmpty()) {
41+
it.addExtension("x-tagGroups", config.tagsConfig.tagGroups.map { tagGroup ->
42+
mapOf(
43+
"name" to tagGroup.name,
44+
"tags" to tagGroup.tags
45+
)
46+
})
47+
}
3848
}
3949
}
4050

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package io.github.smiley4.ktoropenapi.config
2+
3+
import io.github.smiley4.ktoropenapi.data.TagGroupData
4+
5+
/**
6+
* Configuration for a tag group (x-tagGroups OpenAPI vendor extension).
7+
* Tag groups are used to organize tags into logical groups in API documentation.
8+
* See [Redocly x-tagGroups Documentation](https://redocly.com/docs/api-reference-docs/specification-extensions/x-tag-groups/).
9+
*
10+
* Important: All tags used in your API should be included in a tag group, as tags not in any group may not be displayed.
11+
*/
12+
@OpenApiDslMarker
13+
class TagGroupConfig internal constructor(
14+
/**
15+
* The name of the tag group (e.g., "User Management", "Statistics").
16+
*/
17+
var name: String
18+
) {
19+
20+
/**
21+
* List of tag names to include in this group.
22+
* These should match the tag names used in your API operations.
23+
*/
24+
val tags = mutableListOf<String>()
25+
26+
/**
27+
* Add a tag to this group.
28+
*/
29+
fun tag(tagName: String) {
30+
tags.add(tagName)
31+
}
32+
33+
internal fun build(base: TagGroupData) = TagGroupData(
34+
name = name,
35+
tags = buildList {
36+
addAll(base.tags)
37+
addAll(tags)
38+
}
39+
)
40+
41+
}

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package io.github.smiley4.ktoropenapi.config
22

33
import io.github.smiley4.ktoropenapi.data.DataUtils.merge
44
import io.github.smiley4.ktoropenapi.data.TagData
5+
import io.github.smiley4.ktoropenapi.data.TagGroupData
56
import io.github.smiley4.ktoropenapi.data.TagsData
67

78
/**
@@ -11,6 +12,7 @@ import io.github.smiley4.ktoropenapi.data.TagsData
1112
class TagsConfig internal constructor() {
1213

1314
private val tags = mutableListOf<TagConfig>()
15+
private val tagGroups = mutableListOf<TagGroupConfig>()
1416

1517

1618
/**
@@ -21,6 +23,26 @@ class TagsConfig internal constructor() {
2123
}
2224

2325

26+
/**
27+
* Define a tag group for organizing tags in API documentation (x-tagGroups vendor extension).
28+
* Tag groups are particularly useful for tools like Redoc to organize tags in the sidebar.
29+
*
30+
* Important: All tags used in your API should be included in a tag group, as tags not in any group may not be displayed.
31+
*
32+
* Example:
33+
* ```
34+
* tagGroup("User Management") {
35+
* tag("Users")
36+
* tag("API keys")
37+
* tag("Admin")
38+
* }
39+
* ```
40+
*/
41+
fun tagGroup(name: String, block: TagGroupConfig.() -> Unit) {
42+
tagGroups.add(TagGroupConfig(name).apply(block))
43+
}
44+
45+
2446
/**
2547
* Automatically add tags to the route with the given url.
2648
* The returned (non-null) tags will be added to the tags specified in the route-specific documentation.
@@ -37,6 +59,10 @@ class TagsConfig internal constructor() {
3759
addAll(tags.map { it.build(TagData.DEFAULT) })
3860
},
3961
generator = merge(base.generator, tagGenerator) ?: TagsData.DEFAULT.generator,
62+
tagGroups = buildList {
63+
addAll(base.tagGroups)
64+
addAll(tagGroups.map { it.build(TagGroupData.DEFAULT) })
65+
}
4066
)
4167

4268
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package io.github.smiley4.ktoropenapi.data
2+
3+
/**
4+
* Represents a tag group for the x-tagGroups OpenAPI vendor extension.
5+
* Used to organize tags into logical groups in API documentation (e.g., Redoc).
6+
* See [Redocly x-tagGroups Documentation](https://redocly.com/docs/api-reference-docs/specification-extensions/x-tag-groups/).
7+
*/
8+
internal data class TagGroupData(
9+
val name: String,
10+
val tags: List<String>
11+
) {
12+
13+
companion object {
14+
val DEFAULT = TagGroupData(
15+
name = "",
16+
tags = emptyList()
17+
)
18+
}
19+
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ import io.github.smiley4.ktoropenapi.config.TagGenerator
88
internal data class TagsData(
99
val tags: List<TagData>,
1010
val generator: TagGenerator,
11+
val tagGroups: List<TagGroupData>,
1112
) {
1213

1314
companion object {
1415
val DEFAULT = TagsData(
1516
tags = emptyList(),
16-
generator = { emptyList() }
17+
generator = { emptyList() },
18+
tagGroups = emptyList()
1719
)
1820
}
1921

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,45 @@ class OpenApiBuilderTest : StringSpec({
101101
}
102102
}
103103

104+
"x-tagGroups extension" {
105+
val config = OpenApiPluginConfig().also {
106+
it.tags {
107+
tag("Users") {
108+
description = "User management"
109+
}
110+
tag("Products") {
111+
description = "Product catalog"
112+
}
113+
tag("Orders") {
114+
description = "Order processing"
115+
}
116+
117+
tagGroup("E-Commerce") {
118+
tag("Products")
119+
tag("Orders")
120+
}
121+
tagGroup("User Management") {
122+
tag("Users")
123+
}
124+
}
125+
}
126+
buildOpenApiObject(emptyList(), config).also { openapi ->
127+
openapi.extensions shouldNotBe null
128+
openapi.extensions shouldHaveSize 1
129+
openapi.extensions["x-tagGroups"] shouldNotBe null
130+
131+
@Suppress("UNCHECKED_CAST")
132+
val tagGroups = openapi.extensions["x-tagGroups"] as List<Map<String, Any>>
133+
tagGroups shouldHaveSize 2
134+
135+
tagGroups[0]["name"] shouldBe "E-Commerce"
136+
(tagGroups[0]["tags"] as List<*>) shouldContainExactlyInAnyOrder listOf("Products", "Orders")
137+
138+
tagGroups[1]["name"] shouldBe "User Management"
139+
(tagGroups[1]["tags"] as List<*>) shouldContainExactlyInAnyOrder listOf("Users")
140+
}
141+
}
142+
104143
}) {
105144

106145
companion object {

0 commit comments

Comments
 (0)