Skip to content

Commit 742e34b

Browse files
committed
workaround for UnsupportedOperationException (KT-34051)
1 parent 3520f36 commit 742e34b

File tree

3 files changed

+281
-4
lines changed

3 files changed

+281
-4
lines changed

src/main/kotlin/com/papsign/ktor/openapigen/route/path/auth/Functions.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,11 @@ inline fun <reified TParams : Any, reified TResponse : Any, reified TRequest : A
8383
exampleRequest: TRequest? = null,
8484
noinline body: suspend OpenAPIPipelineAuthContext<TAuth, TResponse>.(TParams, TRequest) -> Unit
8585
) {
86+
val paramsType = typeOf<TParams>()
87+
val responseType = typeOf<TResponse>()
88+
val requestType = typeOf<TRequest>()
8689
preHandle<TParams, TResponse, TRequest, OpenAPIAuthenticatedRoute<TAuth>>(exampleResponse, exampleRequest) {
87-
handle(typeOf<TParams>(), typeOf<TResponse>(), typeOf<TRequest>(), body)
90+
handle(paramsType, responseType, requestType, body)
8891
}
8992
}
9093

@@ -93,8 +96,11 @@ inline fun <reified TParams : Any, reified TResponse : Any, TAuth> OpenAPIAuthen
9396
exampleResponse: TResponse? = null,
9497
noinline body: suspend OpenAPIPipelineAuthContext<TAuth, TResponse>.(TParams) -> Unit
9598
) {
99+
val paramsType = typeOf<TParams>()
100+
val responseType = typeOf<TResponse>()
101+
96102
preHandle<TParams, TResponse, Unit, OpenAPIAuthenticatedRoute<TAuth>>(exampleResponse, Unit) {
97-
handle(typeOf<TParams>(), typeOf<TResponse>(), body)
103+
handle(paramsType, responseType, body)
98104
}
99105
}
100106

src/main/kotlin/com/papsign/ktor/openapigen/route/path/normal/Functions.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,11 @@ inline fun <reified TParams : Any, reified TResponse : Any, reified TRequest : A
162162
exampleRequest: TRequest? = null,
163163
noinline body: suspend OpenAPIPipelineResponseContext<TResponse>.(TParams, TRequest) -> Unit
164164
) {
165+
val paramsType = typeOf<TParams>()
166+
val responseType = typeOf<TResponse>()
167+
val requestType = typeOf<TRequest>()
165168
preHandle<TParams, TResponse, TRequest, NormalOpenAPIRoute>(exampleResponse, exampleRequest) {
166-
handle(typeOf<TParams>(), typeOf<TResponse>(), typeOf<TRequest>(), body)
169+
handle(paramsType, responseType, requestType, body)
167170
}
168171
}
169172

@@ -172,7 +175,9 @@ inline fun <reified TParams : Any, reified TResponse : Any> NormalOpenAPIRoute.h
172175
exampleResponse: TResponse? = null,
173176
noinline body: suspend OpenAPIPipelineResponseContext<TResponse>.(TParams) -> Unit
174177
) {
178+
val paramsType = typeOf<TParams>()
179+
val responseType = typeOf<TResponse>()
175180
preHandle<TParams, TResponse, Unit, NormalOpenAPIRoute>(exampleResponse, Unit) {
176-
handle(typeOf<TParams>(), typeOf<TResponse>(), body)
181+
handle(paramsType, responseType, body)
177182
}
178183
}
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
package com.papsign.ktor.openapigen.routing
2+
3+
import com.papsign.ktor.openapigen.annotations.parameters.PathParam
4+
import com.papsign.ktor.openapigen.model.Described
5+
import com.papsign.ktor.openapigen.model.security.HttpSecurityScheme
6+
import com.papsign.ktor.openapigen.model.security.SecuritySchemeModel
7+
import com.papsign.ktor.openapigen.model.security.SecuritySchemeType
8+
import com.papsign.ktor.openapigen.modules.providers.AuthProvider
9+
import com.papsign.ktor.openapigen.route.apiRouting
10+
import com.papsign.ktor.openapigen.route.path.auth.*
11+
import com.papsign.ktor.openapigen.route.path.normal.*
12+
import com.papsign.ktor.openapigen.route.response.respond
13+
import com.papsign.ktor.openapigen.route.route
14+
import com.papsign.ktor.openapigen.route.throws
15+
import installJackson
16+
import installOpenAPI
17+
import io.ktor.application.*
18+
import io.ktor.auth.*
19+
import io.ktor.http.*
20+
import io.ktor.response.*
21+
import io.ktor.server.testing.*
22+
import io.ktor.util.pipeline.*
23+
import org.junit.Test
24+
import java.util.*
25+
import kotlin.test.assertEquals
26+
import kotlin.test.assertTrue
27+
28+
class GenericRoutesTest {
29+
30+
@Test
31+
fun genericRoutesTest() {
32+
withTestApplication({
33+
installOpenAPI()
34+
installJackson()
35+
36+
install(Authentication) {
37+
basic {
38+
realm = "Access to the '/private' path"
39+
validate { credentials ->
40+
if (credentials.name == "jetbrains" && credentials.password == "foobar") {
41+
UserIdPrincipal(credentials.name)
42+
} else {
43+
null
44+
}
45+
}
46+
}
47+
}
48+
49+
50+
val service = ObjectService
51+
apiRouting {
52+
route("objects") {
53+
treeNodeRoute(service)
54+
}
55+
auth {
56+
route("private/objects") {
57+
treeNodeRoutePrivate(service)
58+
}
59+
}
60+
}
61+
}) {
62+
handleRequest(HttpMethod.Get,"/objects").apply {
63+
assertEquals(HttpStatusCode.OK, response.status())
64+
assertTrue { response.contentType().match("application/json") }
65+
}
66+
handleRequest(HttpMethod.Get,"/objects/1").apply {
67+
assertEquals(HttpStatusCode.OK, response.status())
68+
assertTrue { response.contentType().match("application/json") }
69+
}
70+
handleRequest(HttpMethod.Post,"/objects") {
71+
addHeader(HttpHeaders.ContentType, "application/json")
72+
addHeader(HttpHeaders.Accept, "application/json")
73+
setBody(""" { "name": "test" } """)
74+
}.apply {
75+
assertEquals(HttpStatusCode.OK, response.status())
76+
assertTrue { response.contentType().match("application/json") }
77+
}
78+
79+
fun handleRequestWithBasic(method: HttpMethod, uri: String, setup: TestApplicationRequest.() -> Unit = {}): TestApplicationCall {
80+
return handleRequest(method, uri) {
81+
val up = Base64.getEncoder().encodeToString("jetbrains:foobar".toByteArray())
82+
addHeader(HttpHeaders.Authorization, "Basic $up")
83+
setup()
84+
}
85+
}
86+
87+
handleRequest(HttpMethod.Get,"/private/objects").apply {
88+
assertEquals(HttpStatusCode.Unauthorized, response.status())
89+
}
90+
91+
handleRequestWithBasic(HttpMethod.Get,"/private/objects") {
92+
}.apply {
93+
assertEquals(HttpStatusCode.OK, response.status())
94+
assertTrue { response.contentType().match("application/json") }
95+
}
96+
handleRequestWithBasic(HttpMethod.Get,"/private/objects/1").apply {
97+
assertEquals(HttpStatusCode.OK, response.status())
98+
assertTrue { response.contentType().match("application/json") }
99+
}
100+
handleRequestWithBasic(HttpMethod.Post,"/private/objects") {
101+
addHeader(HttpHeaders.ContentType, "application/json")
102+
addHeader(HttpHeaders.Accept, "application/json")
103+
setBody(""" { "name": "test" } """)
104+
}.apply {
105+
assertEquals(HttpStatusCode.OK, response.status())
106+
assertTrue { response.contentType().match("application/json") }
107+
}
108+
109+
}
110+
}
111+
112+
113+
data class ObjectNewDto(override val name: String, override val parentId: Long?) : TreeNodeNew
114+
115+
data class ObjectDto(override val name: String, override val parentId: Long?, override val id: Long): TreeNodeBase
116+
117+
object ObjectService : TreeNodeService<ObjectNewDto, ObjectDto> {
118+
override fun listNodes(parentId: Long?): List<ObjectDto> {
119+
if (parentId == 1L) {
120+
return listOf(
121+
ObjectDto("Node 3", null, 3),
122+
ObjectDto("Node 4", null, 4)
123+
)
124+
}
125+
return listOf(
126+
ObjectDto("Node 1", null, 1),
127+
ObjectDto("Node 2", null, 2)
128+
)
129+
}
130+
131+
override fun createNode(dto: ObjectNewDto): ObjectDto {
132+
return ObjectDto(
133+
dto.name,
134+
dto.parentId,
135+
123
136+
)
137+
}
138+
139+
override fun updateNode(dto: ObjectDto) {
140+
println("updateNode $dto")
141+
}
142+
143+
override fun removeNode(nodeId: Long) {
144+
println("delete $nodeId")
145+
}
146+
147+
}
148+
149+
interface TreeNodeNew {
150+
val name: String
151+
val parentId: Long?
152+
}
153+
154+
interface TreeNodeBase : TreeNodeNew {
155+
val id: Long
156+
}
157+
158+
interface TreeNode : TreeNodeBase {
159+
val children: List<TreeNode>
160+
}
161+
162+
interface TreeNodeService<TNodeNew : TreeNodeNew, TNode : TreeNodeBase> {
163+
fun listNodes(parentId: Long?): List<TNode>
164+
fun createNode(dto: TNodeNew): TNode
165+
fun updateNode(dto: TNode)
166+
fun removeNode(nodeId: Long)
167+
}
168+
169+
data class PathId(@PathParam("Id") val id: Long)
170+
171+
private inline fun <reified TNodeNew : TreeNodeNew, reified TNode : TreeNodeBase> NormalOpenAPIRoute.treeNodeRoute(
172+
service: TreeNodeService<TNodeNew, TNode>
173+
) {
174+
route("{id}").get<PathId, List<TNode>> { params ->
175+
respond(service.listNodes(params.id))
176+
}
177+
route("{id}").delete<PathId, Unit> { params ->
178+
service.removeNode(params.id)
179+
pipeline.call.respond(HttpStatusCode.NoContent)
180+
}
181+
get<Unit, List<TNode>> {
182+
respond(service.listNodes(null))
183+
}
184+
post<Unit, TNode, TNodeNew> { _, body ->
185+
respond(service.createNode(body))
186+
}
187+
patch<Unit, Unit, TNode> { _, body ->
188+
service.updateNode(body)
189+
pipeline.call.respond(HttpStatusCode.NoContent)
190+
}
191+
}
192+
193+
private inline fun <reified TNodeNew : TreeNodeNew, reified TNode : TreeNodeBase> OpenAPIAuthenticatedRoute<UserIdPrincipal>.treeNodeRoutePrivate(
194+
service: TreeNodeService<TNodeNew, TNode>
195+
) {
196+
route("{id}").get<PathId, List<TNode>, UserIdPrincipal> { params ->
197+
respond(service.listNodes(params.id))
198+
}
199+
route("{id}").delete<PathId, Unit, UserIdPrincipal> { params ->
200+
service.removeNode(params.id)
201+
pipeline.call.respond(HttpStatusCode.NoContent)
202+
}
203+
get<Unit, List<TNode>, UserIdPrincipal> {
204+
respond(service.listNodes(null))
205+
}
206+
post<Unit, TNode, TNodeNew, UserIdPrincipal> { _, body ->
207+
respond(service.createNode(body))
208+
}
209+
patch<Unit, Unit, TNode, UserIdPrincipal> { _, body ->
210+
service.updateNode(body)
211+
pipeline.call.respond(HttpStatusCode.NoContent)
212+
}
213+
}
214+
215+
inline fun NormalOpenAPIRoute.auth(route: OpenAPIAuthenticatedRoute<UserIdPrincipal>.() -> Unit): OpenAPIAuthenticatedRoute<UserIdPrincipal> {
216+
return BasicAuthProvider.apply(this).apply(route)
217+
}
218+
219+
class UnauthorizedException(message: String) : RuntimeException(message)
220+
class ForbiddenException(message: String) : RuntimeException(message)
221+
222+
// even if we don't need scopes at all, an empty enum has to be there, see https://github.com/papsign/Ktor-OpenAPI-Generator/issues/65
223+
enum class Scopes : Described
224+
225+
data class ResponseError(val code: Int, val description: String, val message: String? = null) {
226+
constructor(statusCode: HttpStatusCode, message: String? = null) : this(statusCode.value, statusCode.description, message)
227+
}
228+
229+
object BasicAuthProvider : AuthProvider<UserIdPrincipal> {
230+
231+
// description for OpenAPI model
232+
override val security =
233+
listOf(
234+
listOf(
235+
AuthProvider.Security(
236+
SecuritySchemeModel(
237+
name = "basicAuth",
238+
type = SecuritySchemeType.http,
239+
scheme = HttpSecurityScheme.basic
240+
), emptyList<Scopes>()
241+
)
242+
)
243+
)
244+
245+
// gets auth information at runtime
246+
override suspend fun getAuth(pipeline: PipelineContext<Unit, ApplicationCall>): UserIdPrincipal {
247+
return pipeline.context.authentication.principal()
248+
?: throw UnauthorizedException("Unable to verify given credentials, or credentials are missing.")
249+
}
250+
251+
// convert normal route to authenticated route including OpenAPI meta information
252+
override fun apply(route: NormalOpenAPIRoute): OpenAPIAuthenticatedRoute<UserIdPrincipal> {
253+
return OpenAPIAuthenticatedRoute(route.ktorRoute.authenticate { }, route.provider.child(), this)
254+
.throws(
255+
status = HttpStatusCode.Unauthorized.description("Your identity could not be verified."),
256+
gen = { e: UnauthorizedException -> return@throws ResponseError(HttpStatusCode.Unauthorized, e.message) }
257+
)
258+
.throws(
259+
status = HttpStatusCode.Forbidden.description("Your access rights are insufficient."),
260+
gen = { e: ForbiddenException -> return@throws ResponseError(HttpStatusCode.Forbidden, e.message) }
261+
)
262+
}
263+
}
264+
}
265+
266+

0 commit comments

Comments
 (0)