Repository: tipsy/javalin
Pull request #1162 (2021-02-11 02:56:09)

Moving Functions with Duplicate Signatures to Enable Mocking
This pr should address: #1098

This change is not 100% backward compatible, as the extension functions must be imported. It should be a fairly painless change for those using them as the suggestion from IDEs should be to import. With Intellij I was able to just it opt+enter to fix it.

This change will allow users of Javalin to unit test handlers.

tipsy: Thanks @MrThreepwood ! Maybe the benefit outweighs the inconvenice in this case.
tipsy: Can you namespace the extenions with `context` ?

So, instead of 

```
import io.javalin.http.body
import io.javalin.http.pathParam
```

you get

```
import io.javalin.http.context.body
import io.javalin.http.context.pathParam
```
MrThreepwood: Sure, I can do that. I think the alternative to moving them would be to deprecate both the existing ones and add new ones with unique names, which I think would likely be less convenient.

Is there an easy way to namespace things other than placing them in a separate file? I assume you'd want the package to match the file path as well (so add a new directory named context and shove the new files in there?)
tipsy: Thanks @MrThreepwood! I agree that deprication doesn't really add much in this case. I think you should also include the inline functions in `WsContext` in this PR, so we get rid of it all at once?
MrThreepwood: I did not know there was a WsContext. I'll go ahead and do that as well.
tipsy: Thanks, that looks like all of them. I'm still a bit conflicted about if this is okay to release as a bugfix. It is pretty important though, and I don't really have the time to start work on 4.0.0 now 🤔

I'll most likely merge it in the weekened.

Patch:
diff --git a/javalin/src/main/java/io/javalin/http/Context.kt b/javalin/src/main/java/io/javalin/http/Context.kt
index 5ee01e6e..8058ade8 100644
--- a/javalin/src/main/java/io/javalin/http/Context.kt
+++ b/javalin/src/main/java/io/javalin/http/Context.kt
@@ -113,14 +113,6 @@ open class Context(@JvmField val req: HttpServletRequest, @JvmField val res: Htt
     /** Gets the request body as a [String]. */
     fun body(): String = bodyAsBytes().toString(Charset.forName(characterEncoding))
 
-    /**
-     * Maps a JSON body to a Java/Kotlin class using JavalinJson.
-     * JavalinJson can be configured to use any mapping library.
-     * @return The mapped object
-     */
-    @JvmSynthetic
-    inline fun <reified T : Any> body(): T = bodyAsClass(T::class.java)
-
     /** Gets the request body as a [ByteArray].
      * Calling this method returns the body as a [ByteArray]. If [io.javalin.core.JavalinConfig.maxRequestSize]
      * is set and body is bigger than its value, a [io.javalin.http.HttpResponseException] is throw,
@@ -183,10 +175,6 @@ open class Context(@JvmField val req: HttpServletRequest, @JvmField val res: Htt
     @JvmOverloads
     fun <T> formParam(key: String, clazz: Class<T>, default: String? = null) = Validator.create(clazz, formParam(key, default), "Form parameter '$key' with value '${formParam(key, default)}'", key)
 
-    /** Reified version of [formParam] (Kotlin only) */
-    @JvmSynthetic
-    inline fun <reified T : Any> formParam(key: String, default: String? = null) = formParam(key, T::class.java, default)
-
     /** Gets a list of form params for the specified key, or empty list. */
     fun formParams(key: String): List<String> = formParamMap()[key] ?: emptyList()
 
@@ -210,10 +198,6 @@ open class Context(@JvmField val req: HttpServletRequest, @JvmField val res: Htt
      */
     fun <T> pathParam(key: String, clazz: Class<T>) = Validator.create(clazz, pathParam(key), "Path parameter '$key' with value '${pathParam(key)}'", key)
 
-    /** Reified version of [pathParam] (Kotlin only) */
-    @JvmSynthetic
-    inline fun <reified T : Any> pathParam(key: String) = pathParam(key, T::class.java)
-
     /** Gets a map of all the [pathParam] keys and values. */
     fun pathParamMap(): Map<String, String> = Collections.unmodifiableMap(pathParamMap)
 
@@ -260,10 +244,6 @@ open class Context(@JvmField val req: HttpServletRequest, @JvmField val res: Htt
     /** Creates a [Validator] for the header() value, with the prefix "Request header '$header' with the value '$value'" */
     fun <T> header(header: String, clazz: Class<T>): Validator<T> = Validator.create(clazz, header(header), "Request header '$header' with value '${header(header)}'", header)
 
-    /** Reified version of [header] (Kotlin only) */
-    @JvmSynthetic
-    inline fun <reified T : Any> header(header: String) = header(header, T::class.java)
-
     /** Gets a map with all the header keys and values on the request. */
     fun headerMap(): Map<String, String> = req.headerNames.asSequence().associate { it to header(it)!! }
 
@@ -306,10 +286,6 @@ open class Context(@JvmField val req: HttpServletRequest, @JvmField val res: Htt
     @JvmOverloads
     fun <T> queryParam(key: String, clazz: Class<T>, default: String? = null) = Validator.create(clazz, queryParam(key, default), "Query parameter '$key' with value '${queryParam(key, default)}'", key)
 
-    /** Reified version of [queryParam] (Kotlin only) */
-    @JvmSynthetic
-    inline fun <reified T : Any> queryParam(key: String, default: String? = null) = queryParam(key, T::class.java, default)
-
     /** Gets a list of query params for the specified key, or empty list. */
     fun queryParams(key: String): List<String> = queryParamMap()[key] ?: emptyList()
 
@@ -501,3 +477,4 @@ open class Context(@JvmField val req: HttpServletRequest, @JvmField val res: Htt
     /** Gets a list of all the [splat] values. */
     fun splats(): List<String> = Collections.unmodifiableList(splatList)
 }
+
diff --git a/javalin/src/main/java/io/javalin/http/context/Context.kt b/javalin/src/main/java/io/javalin/http/context/Context.kt
new file mode 100644
index 00000000..5ab502fe
--- /dev/null
+++ b/javalin/src/main/java/io/javalin/http/context/Context.kt
@@ -0,0 +1,22 @@
+package io.javalin.http.context
+
+import io.javalin.http.Context
+
+/**
+ * Maps a JSON body to a Java/Kotlin class using JavalinJson.
+ * JavalinJson can be configured to use any mapping library.
+ * @return The mapped object
+ */
+inline fun <reified T : Any> Context.body(): T = bodyAsClass(T::class.java)
+
+/** Reified version of [header] (Kotlin only) */
+inline fun <reified T : Any> Context.header(header: String) = header(header, T::class.java)
+
+/** Reified version of [formParam] (Kotlin only) */
+inline fun <reified T : Any> Context.formParam(key: String, default: String? = null) = formParam(key, T::class.java, default)
+
+/** Reified version of [pathParam] (Kotlin only) */
+inline fun <reified T : Any> Context.pathParam(key: String) = pathParam(key, T::class.java)
+
+/** Reified version of [queryParam] (Kotlin only) */
+inline fun <reified T : Any> Context.queryParam(key: String, default: String? = null) = queryParam(key, T::class.java, default)
diff --git a/javalin/src/main/java/io/javalin/websocket/WsContext.kt b/javalin/src/main/java/io/javalin/websocket/WsContext.kt
index 1cd398da..8bfd9594 100644
--- a/javalin/src/main/java/io/javalin/websocket/WsContext.kt
+++ b/javalin/src/main/java/io/javalin/websocket/WsContext.kt
@@ -37,16 +37,10 @@ abstract class WsContext(val sessionId: String, @JvmField val session: Session)
     fun queryParam(key: String, default: String? = null): String? = upgradeCtx.queryParam(key, default)
     fun <T> queryParam(key: String, clazz: Class<T>, default: String? = null) = upgradeCtx.queryParam(key, clazz, default)
 
-    @JvmSynthetic
-    inline fun <reified T : Any> queryParam(key: String, default: String? = null) = queryParam(key, T::class.java, default)
-
     fun pathParamMap(): Map<String, String> = upgradeCtx.pathParamMap()
     fun pathParam(key: String): String = upgradeCtx.pathParam(key)
     fun <T> pathParam(key: String, clazz: Class<T>) = upgradeCtx.pathParam(key, clazz)
 
-    @JvmSynthetic
-    inline fun <reified T : Any> pathParam(key: String) = pathParam(key, T::class.java)
-
     fun host() = upgradeReq.host // why can't we get this from upgradeCtx?
 
     fun header(header: String): String? = upgradeCtx.header(header)
@@ -86,7 +80,4 @@ class WsBinaryMessageContext(sessionId: String, session: Session, private val da
 class WsMessageContext(sessionId: String, session: Session, private val message: String) : WsContext(sessionId, session) {
     fun message(): String = message
     fun <T> message(clazz: Class<T>): T = JavalinJson.fromJson(message, clazz)
-
-    @JvmSynthetic
-    inline fun <reified T : Any> message(): T = message(T::class.java)
 }
diff --git a/javalin/src/main/java/io/javalin/websocket/context/WsContext.kt b/javalin/src/main/java/io/javalin/websocket/context/WsContext.kt
new file mode 100644
index 00000000..77ee9c62
--- /dev/null
+++ b/javalin/src/main/java/io/javalin/websocket/context/WsContext.kt
@@ -0,0 +1,10 @@
+package io.javalin.websocket.context
+
+import io.javalin.websocket.WsContext
+import io.javalin.websocket.WsMessageContext
+
+inline fun <reified T : Any> WsContext.queryParam(key: String, default: String? = null) = queryParam(key, T::class.java, default)
+
+inline fun <reified T : Any> WsContext.pathParam(key: String) = pathParam(key, T::class.java)
+
+inline fun <reified T : Any> WsMessageContext.message(): T = message(T::class.java)


Test patch:
diff --git a/javalin-openapi/src/test/kotlin/io/javalin/examples/HelloWorldMultiSwagger.kt b/javalin-openapi/src/test/kotlin/io/javalin/examples/HelloWorldMultiSwagger.kt
index 3faf6bbd..e7f7a718 100644
--- a/javalin-openapi/src/test/kotlin/io/javalin/examples/HelloWorldMultiSwagger.kt
+++ b/javalin-openapi/src/test/kotlin/io/javalin/examples/HelloWorldMultiSwagger.kt
@@ -8,6 +8,8 @@ package io.javalin.examples
 import io.javalin.Javalin
 import io.javalin.http.Context
 import io.javalin.http.InternalServerErrorResponse
+import io.javalin.http.context.body
+import io.javalin.http.context.pathParam
 import io.javalin.plugin.openapi.OpenApiOptions
 import io.javalin.plugin.openapi.OpenApiPlugin
 import io.javalin.plugin.openapi.dsl.document
diff --git a/javalin-openapi/src/test/kotlin/io/javalin/examples/HelloWorldSwagger.kt b/javalin-openapi/src/test/kotlin/io/javalin/examples/HelloWorldSwagger.kt
index 4f6f86ec..544d58ee 100644
--- a/javalin-openapi/src/test/kotlin/io/javalin/examples/HelloWorldSwagger.kt
+++ b/javalin-openapi/src/test/kotlin/io/javalin/examples/HelloWorldSwagger.kt
@@ -8,6 +8,8 @@ package io.javalin.examples
 import io.javalin.Javalin
 import io.javalin.http.Context
 import io.javalin.http.InternalServerErrorResponse
+import io.javalin.http.context.body
+import io.javalin.http.context.pathParam
 import io.javalin.plugin.openapi.OpenApiOptions
 import io.javalin.plugin.openapi.OpenApiPlugin
 import io.javalin.plugin.openapi.dsl.document
diff --git a/javalin/src/test/java/io/javalin/TestContextMocking.kt b/javalin/src/test/java/io/javalin/TestContextMocking.kt
new file mode 100644
index 00000000..9b8d0711
--- /dev/null
+++ b/javalin/src/test/java/io/javalin/TestContextMocking.kt
@@ -0,0 +1,27 @@
+package io.javalin
+
+import io.javalin.http.Context
+import io.mockk.every
+import io.mockk.mockk
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+
+class TestContextMocking {
+
+    @Test
+    fun `test context can be mocked`() {
+        val context = mockk<Context>()
+
+        every { context.body() } returns "body"
+        every { context.header(any()) } returns "header"
+        every { context.pathParam(any()) } returns "pathParam"
+        every { context.queryParam(any()) } returns "queryParam"
+        every { context.formParam(any()) } returns "formParam"
+
+        assertThat(context.body()).isEqualTo("body")
+        assertThat(context.header("whatever")).isEqualTo("header")
+        assertThat(context.pathParam("whatever")).isEqualTo("pathParam")
+        assertThat(context.queryParam("whatever")).isEqualTo("queryParam")
+        assertThat(context.formParam("whatever")).isEqualTo("formParam")
+    }
+}
diff --git a/javalin/src/test/java/io/javalin/TestJson.kt b/javalin/src/test/java/io/javalin/TestJson.kt
index 0a557724..71bd1333 100644
--- a/javalin/src/test/java/io/javalin/TestJson.kt
+++ b/javalin/src/test/java/io/javalin/TestJson.kt
@@ -10,6 +10,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
 import com.fasterxml.jackson.databind.SerializationFeature
 import com.google.gson.GsonBuilder
 import com.mashape.unirest.http.Unirest
+import io.javalin.http.context.body
 import io.javalin.plugin.json.FromJsonMapper
 import io.javalin.plugin.json.JavalinJackson
 import io.javalin.plugin.json.JavalinJson
diff --git a/javalin/src/test/java/io/javalin/TestRequest.kt b/javalin/src/test/java/io/javalin/TestRequest.kt
index a97d8c38..d9d311a1 100644
--- a/javalin/src/test/java/io/javalin/TestRequest.kt
+++ b/javalin/src/test/java/io/javalin/TestRequest.kt
@@ -9,6 +9,7 @@ package io.javalin
 import com.mashape.unirest.http.Unirest
 import io.javalin.core.security.BasicAuthFilter
 import io.javalin.core.util.Header
+import io.javalin.http.context.header
 import io.javalin.testing.TestUtil
 import org.assertj.core.api.Assertions.assertThat
 import org.junit.Test
diff --git a/javalin/src/test/java/io/javalin/TestValidation.kt b/javalin/src/test/java/io/javalin/TestValidation.kt
index 6cb894c1..b3b93aa2 100644
--- a/javalin/src/test/java/io/javalin/TestValidation.kt
+++ b/javalin/src/test/java/io/javalin/TestValidation.kt
@@ -12,6 +12,9 @@ import io.javalin.core.validation.JavalinValidation
 import io.javalin.core.validation.Validator
 import io.javalin.core.validation.collectErrors
 import io.javalin.http.BadRequestResponse
+import io.javalin.http.context.formParam
+import io.javalin.http.context.pathParam
+import io.javalin.http.context.queryParam
 import io.javalin.plugin.json.JavalinJson
 import io.javalin.testing.SerializeableObject
 import io.javalin.testing.TestUtil
diff --git a/javalin/src/test/java/io/javalin/TestWebSocket.kt b/javalin/src/test/java/io/javalin/TestWebSocket.kt
index 35310a98..036f0e4d 100644
--- a/javalin/src/test/java/io/javalin/TestWebSocket.kt
+++ b/javalin/src/test/java/io/javalin/TestWebSocket.kt
@@ -15,6 +15,7 @@ import io.javalin.testing.SerializeableObject
 import io.javalin.testing.TestUtil
 import io.javalin.testing.TypedException
 import io.javalin.websocket.WsContext
+import io.javalin.websocket.context.message
 import org.assertj.core.api.Assertions.assertThat
 import org.eclipse.jetty.websocket.api.MessageTooLargeException
 import org.eclipse.jetty.websocket.api.StatusCode