Skip to content

Commit 88738df

Browse files
committed
added opt-in annotation for strict parsing of enum parameters
1 parent 3027d56 commit 88738df

File tree

6 files changed

+243
-19
lines changed

6 files changed

+243
-19
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.papsign.ktor.openapigen.annotations.type.enum
2+
3+
@Target(AnnotationTarget.CLASS)
4+
@Retention(AnnotationRetention.RUNTIME)
5+
annotation class StrictEnumParsing

src/main/kotlin/com/papsign/ktor/openapigen/parameters/parsers/converters/primitive/EnumConverter.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,30 @@
11
package com.papsign.ktor.openapigen.parameters.parsers.converters.primitive
22

3+
import com.papsign.ktor.openapigen.annotations.type.enum.StrictEnumParsing
34
import com.papsign.ktor.openapigen.exceptions.OpenAPIBadContentException
45
import com.papsign.ktor.openapigen.parameters.parsers.converters.Converter
56
import com.papsign.ktor.openapigen.parameters.parsers.converters.ConverterSelector
7+
import kotlin.reflect.KClass
68
import kotlin.reflect.KType
9+
import kotlin.reflect.full.findAnnotation
710
import kotlin.reflect.jvm.jvmErasure
811

912
class EnumConverter(val type: KType) : Converter {
1013

14+
private val isStrictParsing = (type.classifier as? KClass<*>)?.findAnnotation<StrictEnumParsing>() != null
15+
1116
private val enumMap = type.jvmErasure.java.enumConstants.associateBy { it.toString() }
1217

1318
override fun convert(value: String): Any? {
19+
if (!isStrictParsing)
20+
return enumMap[value]
21+
1422
if (enumMap.containsKey(value)) {
1523
return enumMap[value]
1624
} else {
1725
throw OpenAPIBadContentException(
1826
"Invalid value [$value] for enum parameter of type ${type.jvmErasure.simpleName}. Expected: [${
19-
enumMap.values.joinToString(
20-
","
21-
)
27+
enumMap.values.joinToString(",")
2228
}]"
2329
)
2430
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package com.papsign.ktor.openapigen
2+
3+
import com.papsign.ktor.openapigen.annotations.Path
4+
import com.papsign.ktor.openapigen.annotations.parameters.QueryParam
5+
import com.papsign.ktor.openapigen.exceptions.OpenAPIBadContentException
6+
import com.papsign.ktor.openapigen.exceptions.OpenAPIRequiredFieldException
7+
import com.papsign.ktor.openapigen.route.apiRouting
8+
import com.papsign.ktor.openapigen.route.path.normal.get
9+
import com.papsign.ktor.openapigen.route.response.respond
10+
import io.ktor.application.*
11+
import io.ktor.features.*
12+
import io.ktor.http.*
13+
import io.ktor.response.*
14+
import io.ktor.server.testing.*
15+
import kotlin.test.*
16+
17+
enum class NonStrictTestEnum {
18+
VALID,
19+
ALSO_VALID,
20+
}
21+
22+
@Path("/")
23+
data class NullableNonStrictEnumParams(@QueryParam("") val type: NonStrictTestEnum? = null)
24+
25+
@Path("/")
26+
data class NonNullableNonStrictEnumParams(@QueryParam("") val type: NonStrictTestEnum)
27+
28+
class NonStrictEnumTestServer {
29+
30+
companion object {
31+
// test server for nullable enums
32+
private fun Application.nullableEnum() {
33+
install(OpenAPIGen)
34+
install(StatusPages) {
35+
exception<OpenAPIBadContentException> { e ->
36+
call.respond(HttpStatusCode.BadRequest, e.localizedMessage)
37+
}
38+
}
39+
apiRouting {
40+
get<NullableNonStrictEnumParams, String> { params ->
41+
if (params.type != null)
42+
assertTrue { NonStrictTestEnum.values().contains(params.type) }
43+
respond(params.type?.toString() ?: "null")
44+
}
45+
}
46+
}
47+
48+
// test server for non-nullable enums
49+
private fun Application.nonNullableEnum() {
50+
install(OpenAPIGen)
51+
install(StatusPages) {
52+
exception<OpenAPIRequiredFieldException> { e ->
53+
call.respond(HttpStatusCode.BadRequest, e.localizedMessage)
54+
}
55+
exception<OpenAPIBadContentException> { e ->
56+
call.respond(HttpStatusCode.BadRequest, e.localizedMessage)
57+
}
58+
}
59+
apiRouting {
60+
get<NonNullableNonStrictEnumParams, String> { params ->
61+
assertTrue { NonStrictTestEnum.values().contains(params.type) }
62+
respond(params.type.toString())
63+
}
64+
}
65+
}
66+
}
67+
68+
@Test
69+
fun `nullable enum could be omitted and it will be null`() {
70+
withTestApplication({ nullableEnum() }) {
71+
handleRequest(HttpMethod.Get, "/").apply {
72+
assertEquals(HttpStatusCode.OK, response.status())
73+
assertEquals("null", response.content)
74+
}
75+
}
76+
}
77+
78+
@Test
79+
fun `nullable enum should be parsed correctly`() {
80+
withTestApplication({ nullableEnum() }) {
81+
handleRequest(HttpMethod.Get, "/?type=VALID").apply {
82+
assertEquals(HttpStatusCode.OK, response.status())
83+
assertEquals("VALID", response.content)
84+
}
85+
handleRequest(HttpMethod.Get, "/?type=ALSO_VALID").apply {
86+
assertEquals(HttpStatusCode.OK, response.status())
87+
assertEquals("ALSO_VALID", response.content)
88+
}
89+
}
90+
}
91+
92+
@Test
93+
fun `nullable enum parsing should be case-sensitive and should return 200 with null result`() {
94+
withTestApplication({ nullableEnum() }) {
95+
handleRequest(HttpMethod.Get, "/?type=valid").apply {
96+
assertEquals(HttpStatusCode.OK, response.status())
97+
assertEquals("null", response.content)
98+
}
99+
handleRequest(HttpMethod.Get, "/?type=also_valid").apply {
100+
assertEquals(HttpStatusCode.OK, response.status())
101+
assertEquals("null", response.content)
102+
}
103+
}
104+
}
105+
106+
@Test
107+
fun `nullable enum parsing should return 200 with null result on parse values outside of enum`() {
108+
withTestApplication({ nullableEnum() }) {
109+
handleRequest(HttpMethod.Get, "/?type=what").apply {
110+
assertEquals(HttpStatusCode.OK, response.status())
111+
assertEquals("null", response.content)
112+
}
113+
}
114+
}
115+
116+
@Test
117+
fun `non-nullable enum cannot be omitted`() {
118+
withTestApplication({ nonNullableEnum() }) {
119+
handleRequest(HttpMethod.Get, "/").apply {
120+
assertEquals(HttpStatusCode.BadRequest, response.status())
121+
assertEquals("The field type is required", response.content)
122+
}
123+
}
124+
}
125+
126+
@Test
127+
fun `non-nullable enum should be parsed correctly`() {
128+
withTestApplication({ nonNullableEnum() }) {
129+
handleRequest(HttpMethod.Get, "/?type=VALID").apply {
130+
assertEquals(HttpStatusCode.OK, response.status())
131+
assertEquals("VALID", response.content)
132+
}
133+
handleRequest(HttpMethod.Get, "/?type=ALSO_VALID").apply {
134+
assertEquals(HttpStatusCode.OK, response.status())
135+
assertEquals("ALSO_VALID", response.content)
136+
}
137+
}
138+
}
139+
140+
@Test
141+
fun `non-nullable enum parsing should be case-sensitive and should throw on passing wrong case`() {
142+
withTestApplication({ nonNullableEnum() }) {
143+
handleRequest(HttpMethod.Get, "/?type=valid").apply {
144+
assertEquals(HttpStatusCode.BadRequest, response.status())
145+
assertEquals("The field type is required", response.content)
146+
}
147+
handleRequest(HttpMethod.Get, "/?type=also_valid").apply {
148+
assertEquals(HttpStatusCode.BadRequest, response.status())
149+
assertEquals("The field type is required", response.content)
150+
}
151+
}
152+
}
153+
154+
@Test
155+
fun `non-nullable enum parsing should not parse values outside of enum`() {
156+
withTestApplication({ nonNullableEnum() }) {
157+
handleRequest(HttpMethod.Get, "/?type=what").apply {
158+
assertEquals(HttpStatusCode.BadRequest, response.status())
159+
assertEquals("The field type is required", response.content)
160+
}
161+
}
162+
}
163+
}

src/test/kotlin/com/papsign/ktor/openapigen/EnumTestServer.kt renamed to src/test/kotlin/com/papsign/ktor/openapigen/EnumStrictTestServer.kt

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.papsign.ktor.openapigen
22

33
import com.papsign.ktor.openapigen.annotations.Path
44
import com.papsign.ktor.openapigen.annotations.parameters.QueryParam
5+
import com.papsign.ktor.openapigen.annotations.type.enum.StrictEnumParsing
56
import com.papsign.ktor.openapigen.exceptions.OpenAPIBadContentException
67
import com.papsign.ktor.openapigen.exceptions.OpenAPIRequiredFieldException
78
import com.papsign.ktor.openapigen.route.apiRouting
@@ -14,18 +15,19 @@ import io.ktor.response.*
1415
import io.ktor.server.testing.*
1516
import kotlin.test.*
1617

17-
enum class TestEnum {
18+
@StrictEnumParsing
19+
enum class StrictTestEnum {
1820
VALID,
1921
ALSO_VALID,
2022
}
2123

2224
@Path("/")
23-
data class NullableEnumParams(@QueryParam("") val type: TestEnum? = null)
25+
data class NullableStrictEnumParams(@QueryParam("") val type: StrictTestEnum? = null)
2426

2527
@Path("/")
26-
data class NonNullableEnumParams(@QueryParam("") val type: TestEnum)
28+
data class NonNullableStrictEnumParams(@QueryParam("") val type: StrictTestEnum)
2729

28-
class EnumTestServer {
30+
class EnumStrictTestServer {
2931

3032
companion object {
3133
// test server for nullable enums
@@ -37,9 +39,9 @@ class EnumTestServer {
3739
}
3840
}
3941
apiRouting {
40-
get<NullableEnumParams, String> { params ->
42+
get<NullableStrictEnumParams, String> { params ->
4143
if (params.type != null)
42-
assertTrue { TestEnum.values().contains(params.type) }
44+
assertTrue { StrictTestEnum.values().contains(params.type) }
4345
respond(params.type?.toString() ?: "null")
4446
}
4547
}
@@ -57,8 +59,8 @@ class EnumTestServer {
5759
}
5860
}
5961
apiRouting {
60-
get<NonNullableEnumParams, String> { params ->
61-
assertTrue { TestEnum.values().contains(params.type) }
62+
get<NonNullableStrictEnumParams, String> { params ->
63+
assertTrue { StrictTestEnum.values().contains(params.type) }
6264
respond(params.type.toString())
6365
}
6466
}
@@ -95,14 +97,14 @@ class EnumTestServer {
9597
handleRequest(HttpMethod.Get, "/?type=valid").apply {
9698
assertEquals(HttpStatusCode.BadRequest, response.status())
9799
assertEquals(
98-
"Invalid value [valid] for enum parameter of type TestEnum. Expected: [VALID,ALSO_VALID]",
100+
"Invalid value [valid] for enum parameter of type StrictTestEnum. Expected: [VALID,ALSO_VALID]",
99101
response.content
100102
)
101103
}
102104
handleRequest(HttpMethod.Get, "/?type=also_valid").apply {
103105
assertEquals(HttpStatusCode.BadRequest, response.status())
104106
assertEquals(
105-
"Invalid value [also_valid] for enum parameter of type TestEnum. Expected: [VALID,ALSO_VALID]",
107+
"Invalid value [also_valid] for enum parameter of type StrictTestEnum. Expected: [VALID,ALSO_VALID]",
106108
response.content
107109
)
108110
}
@@ -115,7 +117,7 @@ class EnumTestServer {
115117
handleRequest(HttpMethod.Get, "/?type=what").apply {
116118
assertEquals(HttpStatusCode.BadRequest, response.status())
117119
assertEquals(
118-
"Invalid value [what] for enum parameter of type TestEnum. Expected: [VALID,ALSO_VALID]",
120+
"Invalid value [what] for enum parameter of type StrictTestEnum. Expected: [VALID,ALSO_VALID]",
119121
response.content
120122
)
121123
}
@@ -152,14 +154,14 @@ class EnumTestServer {
152154
handleRequest(HttpMethod.Get, "/?type=valid").apply {
153155
assertEquals(HttpStatusCode.BadRequest, response.status())
154156
assertEquals(
155-
"Invalid value [valid] for enum parameter of type TestEnum. Expected: [VALID,ALSO_VALID]",
157+
"Invalid value [valid] for enum parameter of type StrictTestEnum. Expected: [VALID,ALSO_VALID]",
156158
response.content
157159
)
158160
}
159161
handleRequest(HttpMethod.Get, "/?type=also_valid").apply {
160162
assertEquals(HttpStatusCode.BadRequest, response.status())
161163
assertEquals(
162-
"Invalid value [also_valid] for enum parameter of type TestEnum. Expected: [VALID,ALSO_VALID]",
164+
"Invalid value [also_valid] for enum parameter of type StrictTestEnum. Expected: [VALID,ALSO_VALID]",
163165
response.content
164166
)
165167
}
@@ -172,7 +174,7 @@ class EnumTestServer {
172174
handleRequest(HttpMethod.Get, "/?type=what").apply {
173175
assertEquals(HttpStatusCode.BadRequest, response.status())
174176
assertEquals(
175-
"Invalid value [what] for enum parameter of type TestEnum. Expected: [VALID,ALSO_VALID]",
177+
"Invalid value [what] for enum parameter of type StrictTestEnum. Expected: [VALID,ALSO_VALID]",
176178
response.content
177179
)
178180
}

src/test/kotlin/com/papsign/ktor/openapigen/parameters/parsers/builder/query/form/EnumBuilderTest.kt

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
11
package com.papsign.ktor.openapigen.parameters.parsers.builder.query.form
22

3+
import com.papsign.ktor.openapigen.annotations.type.enum.StrictEnumParsing
34
import com.papsign.ktor.openapigen.exceptions.OpenAPIBadContentException
45
import com.papsign.ktor.openapigen.getKType
56
import com.papsign.ktor.openapigen.parameters.parsers.builders.query.form.FormBuilderFactory
67
import com.papsign.ktor.openapigen.parameters.parsers.testSelector
78
import org.junit.Test
89
import kotlin.test.assertFailsWith
910
import kotlin.test.assertNotNull
11+
import kotlin.test.assertNull
1012

1113
class EnumBuilderTest {
1214

1315
enum class TestEnum {
1416
A, B, C
1517
}
1618

19+
@StrictEnumParsing
20+
enum class StrictTestEnum {
21+
A, B, C
22+
}
23+
1724
@Test
1825
fun testEnum() {
1926
val key = "key"
@@ -25,9 +32,26 @@ class EnumBuilderTest {
2532
}
2633

2734
@Test
28-
fun `should throw on enum value outside of enum`() {
35+
fun testStrictEnum() {
36+
val key = "key"
37+
val expected = StrictTestEnum.B
38+
val parse = mapOf(
39+
key to listOf("B")
40+
)
41+
FormBuilderFactory.testSelector(expected, key, parse, true)
42+
}
43+
44+
@Test
45+
fun `should NOT throw on enum value outside of enum without StrictParsing and return null`() {
2946
val type = getKType<TestEnum>()
3047
val builder = assertNotNull(FormBuilderFactory.buildBuilder(type, true))
48+
assertNull(builder.build("key", mapOf("key" to listOf("XXX"))))
49+
}
50+
51+
@Test
52+
fun `should throw on enum value outside of enum with StrictParsing`() {
53+
val type = getKType<StrictTestEnum>()
54+
val builder = assertNotNull(FormBuilderFactory.buildBuilder(type, true))
3155
assertFailsWith<OpenAPIBadContentException> {
3256
builder.build("key", mapOf("key" to listOf("XXX")))
3357
}

0 commit comments

Comments
 (0)