Ktor Location API: mapping JSON to generic Map object - ktor

When I use the regular routing API together with GSON, I can deserialize a JSON parameter to a Map<String, Any> with the following code snippet:
post("/books") {
val request = call.receive<Map<String, Any>>()
...
}
In my case request is an instance of com.google.gson.internal.LinkedTreeMap.
Is there a way to do the same using the Location API? It works fine when I define a data class with concrete members but I can't find a way to use a map. I'm trying it with a couple of things along those lines:
#Location ("/books")
<some magic class definition>
fun Application.bookModule() {
routing {
post<BookRequest> {
val request = call.receive<BookRequest>()
...
}
but I've not come up with anything that works. Help?

Locations represent path and query parameters of endpoints only, so there is no way to route depending on the request body. Properties of a location class should be mapped to path segments, e.g.
#Location ("/books/{title}/{author}") data class BookRequest(
val title: String,
val author: String,
)
To receive a request body and convert to it an object just use the ContentNegotiation plugin. Here is the example of a server that responds with 200 OK to the curl -v --header "Content-Type: application/json" --request POST --data '{"title":"Hitchhiker", "author":"DNA", "detail":{"foo": "bar"}}' http://localhost:8080/books request:
import io.ktor.application.*
import io.ktor.features.*
import io.ktor.gson.*
import io.ktor.http.*
import io.ktor.locations.*
import io.ktor.locations.post
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.routing
import io.ktor.server.engine.*
import io.ktor.server.netty.*
fun main(args: Array<String>) {
embeddedServer(Netty, port = 8080) {
install(ContentNegotiation) {
gson()
}
install(Locations)
bookModule()
}.start()
}
fun Application.bookModule() {
routing {
post<BookRequest> {
val request = call.receive<Map<String, Any>>()
println(request)
call.respond(HttpStatusCode.OK)
}
}
}
#Location ("/books") class BookRequest

Related

How to set multiple params in this type "Class...parameterTypes" when building Micronaut programmatically routes -io.micronaut.web.router.RouteBuilder

I'm trying to build routes for my service with Micronaut. Following this tutorial the example works fine.
This is my controller:
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.PathVariable
#Controller
class DemoController {
#Get
fun issue(
#PathVariable a: String): String {
return "Issue # $a"
}
}
And this is my route class:
import io.micronaut.context.ExecutionHandleLocator
import io.micronaut.web.router.DefaultRouteBuilder
import io.micronaut.web.router.RouteBuilder
import jakarta.inject.Inject
import jakarta.inject.Singleton
#Singleton
class MyRoutes(executionHandleLocator: ExecutionHandleLocator,
uriNamingStrategy: RouteBuilder.UriNamingStrategy) :
DefaultRouteBuilder(executionHandleLocator, uriNamingStrategy) {
#Inject
fun issuesRoutes(demoController: DemoController) {
GET("/issues/show/{number}", demoController, "issue", String::class.java)
}
}
Everything working fine so far.
The problem is that I have more than one parameter in the endpoint. For example:
#Controller
class DemoController {
#Get
fun issue(
#PathVariable a: String,
#PathVariable b: String
): String {
return "Issue # $a"
}
}
In MyRoutes class, on the function issuesRoutes, I need to set the parameterTypes for 2 params now, and I don't know how should I do it.
The documentation of RouteBuilder says as follow:
Route the specified URI template to the specified target.
The number of variables in the template should match the number of method arguments
Params:
uri – The URI
target – The target
method – The method
**parameterTypes – The parameter types for the target method**
Returns:The route
#Override
public UriRoute GET(String uri, Object target, String method, Class... parameterTypes) {
return buildRoute(HttpMethod.GET, uri, target.getClass(), method, parameterTypes);
}
How could I tell the method the types of my two string params (#PathVariables) in this kind of param the method is expecting (Class... parameterTypes).
The error I get with this configuration is:
Caused by: io.micronaut.web.router.exceptions.RoutingException: No such route: com.example.rest.DemoController.issue
Given a controller:
#Controller
class DemoController {
#Get
fun issue(#PathVariable a: String, #PathVariable b: String) = "Issue # a=$a b=$b"
}
The route would be:
#Singleton
class MyRoutes(executionHandleLocator: ExecutionHandleLocator,
uriNamingStrategy: RouteBuilder.UriNamingStrategy) :
DefaultRouteBuilder(executionHandleLocator, uriNamingStrategy) {
#Inject
fun issuesRoutes(demoController: DemoController) {
GET("/issues/show/{a}/{b}", // uri
demoController, // target
"issue", // method
String::class.java, String::class.java) //vararg parameterTypes:Class
}
}

Sample for REST API using Location and JSON

The title already says what I'm trying to do in a nutshell: implement a simple REST API that uses ktor's Location feature and accepts requests with JSON as payload.
Let's say that I want to have a resource "books" that is available under the URI /books. A get request should return a list of available books, a post request with data for a new book should create a new book and return a redirect to the newly created book. My implementation looks like this:
#Location ("/books") data class BookRequest(val title: String = "" )
fun Application.bookModule() {
routing {
get<BookRequest> {
val books = bookHandler.listBooks()
//generate HTML from books and respond
}
post<BookRequest> {
val request = call.receive<BookRequest>()
//create a new book resource from the data in request
//and respond with redirect to new book
}
}
The get request works as intended but when I try to POST a new book using curl like this
curl --header "Content-Type: application/json" \
--request POST \
--data '{"title":"Hitchhiker"}' \
http://localhost:8080/books
the content of the title attribute of the request is empty.
Does anyone have a pointer to a working example using Locations with POST and JSON?
You need to install ContentNegotiation plugin for deserializing JSON data to a BookRequest object and if you use kotlinx.serialization then mark the BookRequest class with Serializable annotation. Here is the full example:
import io.ktor.application.*
import io.ktor.features.*
import io.ktor.locations.*
import io.ktor.locations.post
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.routing
import io.ktor.serialization.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import kotlinx.serialization.Serializable
fun main(args: Array<String>) {
embeddedServer(Netty, port = 8080) {
install(ContentNegotiation) {
json()
}
install(Locations)
routing {
post<BookRequest> {
val r = call.receive<BookRequest>()
call.respondText { r.title }
}
}
}.start()
}
#Location("/books")
#Serializable
data class BookRequest(val title: String = "")

Is there any way to make a fake call from Ktor to itself, to make request pass through all pipeline?

I have an ktor web server that successfully responds on http requests. Now there is a need to read data from kafka's topic and process it.
Is there any way send the data I've read to ktor, like this data came from outside, to make it pass through all pipeline, like ContentNegotiation and other features?
Application class has method execute(), which takes ApplicationCall, but I've found zero examples - how can I fill my implementation of this class properly. Especially route - do I need the real one? Would be nice if this route would be private and would be unavailable from the outside.
You can use the withTestApplication function to test your application's modules without making an actual network connection. Here is an example:
import io.ktor.application.*
import io.ktor.http.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.testing.*
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
class SimpleTest {
#Test
fun test() = withTestApplication {
application.module()
// more modules to test here
handleRequest(HttpMethod.Post, "/post") {
setBody("kafka data")
}.response.let { response ->
assertEquals("I get kafka data", response.content)
}
}
}
fun Application.module() {
routing {
post("/post") {
call.respondText { "I get ${call.receiveText()}" }
}
}
}
I think that #AlekseiTirman answer is great and most probably you should go for it
But I have to mention that it's easy to do it even in "real life" run. Your local machine ip is 0.0.0.0, you can get port from the env variable, so you just can create a simple HttpClient and send a request:
CoroutineScope(Dispatchers.IO).launch {
delay(1000)
val client = HttpClient {
defaultRequest {
// actually this is already a default value so no need to setting it
host = "0.0.0.0"
port = environment.config.property("ktor.deployment.port").getString().toInt()
}
}
val result = client.get<String>("good")
println("local response $result")
}
routing {
get("good") {
call.respond("hello world")
}
}

Get request to Ktor Location results in an Unhandled request

I just want to use the Locations Feature of Ktor for my API and I tested it with two simple get requests. The problem is that Ktor seems to ignore my #Locations and if I use the Logging feature it says that the request is unhandled:
TRACE Application - Unhandled: GET - /register
My Application class:
import io.ktor.application.*
import io.ktor.features.*
import io.ktor.gson.*
import io.ktor.locations.*
import io.ktor.response.*
import io.ktor.routing.*
fun Application.main() {
install(Locations)
install(CallLogging)
install(ContentNegotiation) {
gson {
setPrettyPrinting()
}
}
routing {
get("/health_check") {
call.respondText("OK")
}
auth()
}
}
Auth class:
import io.ktor.application.*
import io.ktor.locations.*
import io.ktor.response.*
import io.ktor.routing.*
#Location("/register")
data class Register(val username: String, val email: String, val password: String)
#Location("/login")
data class Login(val username: String, val password: String)
fun Route.auth() {
get<Register> {
call.respondText("Register")
}
get<Login> {
call.respondText("Login")
}
}
I tried to find some information about my problem, but I haven't found any. I thought it might have something to do with the fact that the Locations API of Ktor is still experimental. I hope this is enough information. Thanks in advance.

How to create a default body for a Retrofit call directly in the interface?

I would like to add a default body to a single Retrofit call inside the interface I made.
Let's say I have a Retrofit interface such as:
import retrofit2.Call
import retrofit2.http.*
interface ExampleAPI {
#POST
fun makeRequest(): Call<SomeResponse>
}
And I would like to add a default body to the request with fields such as:
param_one: j32n4n4jt
param_two: k23n45k43t
I am aware I can wrap the generated function and inject the body via:
import retrofit2.Call
import retrofit2.http.*
interface ExampleAPI {
#POST
fun makeRequest(#Body body: Map<String, String>): Call<SomeResponse>
}
or I can make a if check in an interceptor.
However, is it possible to implement this directly in the interface, and if so, how?
Try this
Generic Request:
open class CommonRequest(
#Ignore
#SerializedName("param_one") val param_one: String = "j32n4n4jt",
#Ignore
#SerializedName("param_two") val param_two: String = "j32n4n4jt"
)
Actual Request:
data class Request(
#SerializedName("name") val name: String = "Andrew",
) : CommonRequest()
Usage:
interface ExampleAPI {
#POST
fun makeRequest(#Body request: Request): Call<SomeResponse>
}