Skip to content

Commit 3b3429f

Browse files
authored
Merge pull request #113 from Szer/enum-validation
Added validation error on parsing enum values outside of valid enum values
2 parents 60f21e7 + 88738df commit 3b3429f

File tree

6 files changed

+445
-4
lines changed

6 files changed

+445
-4
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: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,36 @@
11
package com.papsign.ktor.openapigen.parameters.parsers.converters.primitive
22

3+
import com.papsign.ktor.openapigen.annotations.type.enum.StrictEnumParsing
4+
import com.papsign.ktor.openapigen.exceptions.OpenAPIBadContentException
35
import com.papsign.ktor.openapigen.parameters.parsers.converters.Converter
46
import com.papsign.ktor.openapigen.parameters.parsers.converters.ConverterSelector
7+
import kotlin.reflect.KClass
58
import kotlin.reflect.KType
9+
import kotlin.reflect.full.findAnnotation
610
import kotlin.reflect.jvm.jvmErasure
711

8-
class EnumConverter(type: KType): Converter {
12+
class EnumConverter(val type: KType) : Converter {
13+
14+
private val isStrictParsing = (type.classifier as? KClass<*>)?.findAnnotation<StrictEnumParsing>() != null
915

1016
private val enumMap = type.jvmErasure.java.enumConstants.associateBy { it.toString() }
1117

1218
override fun convert(value: String): Any? {
13-
return enumMap[value]
19+
if (!isStrictParsing)
20+
return enumMap[value]
21+
22+
if (enumMap.containsKey(value)) {
23+
return enumMap[value]
24+
} else {
25+
throw OpenAPIBadContentException(
26+
"Invalid value [$value] for enum parameter of type ${type.jvmErasure.simpleName}. Expected: [${
27+
enumMap.values.joinToString(",")
28+
}]"
29+
)
30+
}
1431
}
1532

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

0 commit comments

Comments
 (0)