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

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>
}

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
}
}

Ktor Location API: mapping JSON to generic Map object

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

aws-lambda handler with InputStream com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of java.io.InputStream

I put my AWS Lambda behind API gateway, and now trying to make an end-to-end call.
import java.io.InputStream
import com.amazonaws.services.lambda.runtime.{Context, RequestHandler}
import com.fasterxml.jackson.databind
case class MyClass(a: String, b: String)
class MyHandler extends RequestHandler[InputStream, Boolean] {
val scalaMapper: databind.ObjectMapper = {
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule
new ObjectMapper().registerModule(new DefaultScalaModule)
}
def handleRequest(input: InputStream, context: Context): Boolean = {
val myClass = scalaMapper.readValue(input, classOf[MyClass])
isValid(myClass)
}
It works when I test locally by providing the handler with a string, but when in a Lambda, the handler can't use the input stream. I'm getting the error
Endpoint response body before transformations: {
"errorMessage":"An error occurred during JSON parsing",
"errorType":"java.lang.RuntimeException",
"stackTrace":[],
"cause": {
"errorMessage":"com.fasterxml.jackson.databind.JsonMappingException:
Can not construct instance of java.io.InputStream,
problem: abstract types either need to be mapped to concrete types, have custom deserializer, or be instantiated with additional type information\n
at [Source: lambdainternal.util.NativeMemoryAsInputStream#2698dc7; line: 1, column: 1]",...
A Java Lambda has two possible signatures:
public interface RequestHandler<I, O> {
public O handleRequest(I input, Context context);
}
and
public interface RequestStreamHandler {
public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException;
}
I'm not sure if you're trying to mix the two but you're trying to tell Lambda to deserialize an InputStream.
While I try hard not to do anything in Scala, I believe you want:
case class MyClass(a: String, b: String)
class MyHandler extends RequestStreamHandler {
val scalaMapper: databind.ObjectMapper = {
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule
new ObjectMapper().registerModule(new DefaultScalaModule)
}
def handleRequest(input: InputStream, output: OutputStream context: Context): Unit = {
val myClass = scalaMapper.readValue(input, classOf[MyClass])
output.write( isValid(myClass) )
}
Though I have not tested this. Another, perhaps better way would be:
case class MyClass(a: String, b: String)
class MyHandler extends RequestHandler[MyClass, Boolean] {
def handleRequest(myClass: MyClass, context: Context): Boolean = {
isValid(myClass)
}

How to handle properly body params in #post request when using data classed with generics

I'm trying to create endpoint for a post request:
#Singleton
#Controller("/v1")
class Addr() {
#Post("/setAddress")
fun set(#Body body: RpcRequest<Address>) {
println(body.params.newAddress)
}
}
Data:
data class Address(val newAddress: String)
#JsonIgnoreProperties(ignoreUnknown = true)
data class RpcRequest<T>(
val method: String,
val params: T
)
But unfortunately, error occured:
java.util.LinkedHashMap cannot be cast to com.project.location.data.Address
Seems like Micronaut couldn't resolve my Address data class inside RpcRequest ?

Using data class in micronaut properties

I'm writing configuration properties and would like to use data class to hold the data.
Problem is, data classes have a primary constructor and are immutable, and micronaut tries to inject the values as beans.
Example:
#ConfigurationProperties("gerencianet")
data class GerenciaNetConfiguration(
val clientId: String,
val clientSecret: String,
val apiUrl: String,
val notificationUrl: String,
val datePattern: String = "yyyy-MM-dd HH:mm:ss"
)
Error: Caused by: io.micronaut.context.exceptions.NoSuchBeanException: No bean of type [java.lang.String] exists. Make sure the bean is not disabled by bean requirements
Is there support for it?
You can inject the values as constructor parameters using #Parameter. It avoids common mistakes with #Value.
For example, if your application.yml looks like this:
group:
first-value: asdf
second-value: ghjk
Then a Kotlin class might look like:
import io.micronaut.context.annotation.Property
import javax.inject.Singleton
#Singleton
class MyClass(#Property(name = "group.first-value") val firstValue: String) {
fun doIt(): String {
return firstValue
}
}
Or, similarly, a method:
import io.micronaut.context.annotation.Factory
import io.micronaut.context.annotation.Property
import javax.inject.Singleton
#Factory
class MyFactory {
#Singleton
fun getSomeValue(#Property(name = "group.first-value") firstValue: String): SomeClass {
return SomeClass.newBuilder()
.setTheValue(firstValue)
.build()
}
}
One option you have is to do something like this:
import io.micronaut.context.annotation.ConfigurationBuilder
import io.micronaut.context.annotation.ConfigurationProperties
#ConfigurationProperties("my.engine")
internal class EngineConfig {
#ConfigurationBuilder(prefixes = ["with"])
val builder = EngineImpl.builder()
#ConfigurationBuilder(prefixes = ["with"], configurationPrefix = "crank-shaft") / <3>
val crankShaft = CrankShaft.builder()
#set:ConfigurationBuilder(prefixes = ["with"], configurationPrefix = "spark-plug")
var sparkPlug = SparkPlug.builder()
}
That is from our test suite at https://github.com/micronaut-projects/micronaut-core/blob/1c3e2c3280da200c96e629a4edb9df87875ef2ff/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/config/builder/EngineConfig.kt.
You can also inject the values as constructor parameters using #Value.
I hope that helps.