ktor-client : how to serialize a post body as a specific type - ktor

With ktor client, I've got a non-serializable object derived from a serializable object like so:
#Serializable
#SerialName("login-request")
open class LoginRequest (
open val email : String = "",
open val password : String = ""
) : ServiceRequestPayload
impl class
class LoginRequestVo : LoginRequest("", ""), NodeComponent by NodeComponentImpl() {
override val email: String by subject("")
override val password: String by subject("")
}
Now when I manually use kotlinx serialization on this like so:
val request : LoginRequest = LoginRequestVo().apply {
email = "test#gmail.com"
password = "password"
}
val str = Json.encodeToString(request)
println(str)
It correctly serializes it, and when deserialized on the other side, it correctly deserializes to LoginRequest. However, when I use ktor-client to serialize my object, it complains that LoginRequestVo is not serializable. The example code below uses some other objects from my project and has more information that you need, but the gist is that the type of U in the invoke function and therefore the request.payload expression is type LoginRequest as specified by the LoginServiceImpl below.
suspend inline fun <T, U: ServiceRequestPayload> HttpClient.invoke(desc : EndpointDescriptor<T, U>, request : ServiceRequest<U>? ) : ServiceResponse {
val path = "/api${desc.path}"
return when(desc.method) {
HttpMethod.GET -> {
get(path)
}
HttpMethod.POST -> {
if (request == null) {
post(path)
} else {
post(path) {
contentType(ContentType.Application.Json)
body = request.payload
}
}
}
HttpMethod.DELETE -> delete(desc.path)
HttpMethod.PUT -> {
if (request == null) {
put(path)
} else {
post(path) {
contentType(ContentType.Application.Json)
body = request.payload
}
}
}
}
}
class LoginServiceImpl(
context : ApplicationContext
) : LoginService {
private val client by context.service<HttpClient>()
override suspend fun login(request: ServiceRequest<LoginRequest>) = client.invoke(LoginServiceDesc.login, request)
override suspend fun register(request: ServiceRequest<LoginRequest>) = client.invoke(LoginServiceDesc.register, request)
}
The error I get is:
My question is, Is there a way to specify to ktor-client the type or the serializer to use when it needs to serialize a body?

Related

How to create an HttpResponse object with dummy values in ktor Kotlin?

I am using ktor for developing a microservice in Kotlin. For testing a method, I need to create a dummy HttpResponse (io.ktor.client.statement.HttpResponse to be specific) object with status = 200 and body = some json data.
Any idea how I can create it?
You can use mockk or a similar kind of library to mock an HttpResponse. Unfortunately, this is complicated because HttpRequest, HttpResponse, and HttpClient objects are tightly coupled with the HttpClientCall. Here is an example of how you can do that:
val call = mockk<HttpClientCall> {
every { client } returns mockk {}
coEvery { receive(io.ktor.util.reflect.typeInfo<String>()) } returns "body"
every { coroutineContext } returns EmptyCoroutineContext
every { attributes } returns Attributes()
every { request } returns object : HttpRequest {
override val call: HttpClientCall = this#mockk
override val attributes: Attributes = Attributes()
override val content: OutgoingContent = object : OutgoingContent.NoContent() {}
override val headers: Headers = Headers.Empty
override val method: HttpMethod = HttpMethod.Get
override val url: Url = Url("/")
}
every { response } returns object : HttpResponse() {
override val call: HttpClientCall = this#mockk
override val content: ByteReadChannel = ByteReadChannel("body")
override val coroutineContext: CoroutineContext = EmptyCoroutineContext
override val headers: Headers = Headers.Empty
override val requestTime: GMTDate = GMTDate.START
override val responseTime: GMTDate = GMTDate.START
override val status: HttpStatusCode = HttpStatusCode.OK
override val version: HttpProtocolVersion = HttpProtocolVersion.HTTP_1_1
}
}
val response = call.response
I did this with following. I only needed to pass a status code and description, so I didn't bother about other fields.
class CustomHttpResponse(
private val statusCode: Int,
private val description: String
) :
HttpResponse() {
#InternalAPI
override val content: ByteReadChannel
get() = ByteReadChannel("")
override val call: HttpClientCall
get() = HttpClientCall(HttpClient())
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
override val headers: Headers
get() = Headers.Empty
override val requestTime: GMTDate
get() = GMTDate()
override val responseTime: GMTDate
get() = GMTDate()
override val status: HttpStatusCode
get() = HttpStatusCode(statusCode, description)
override val version: HttpProtocolVersion
get() = HttpProtocolVersion(name = "HTTP", major = 1, minor = 1)}
With Ktor 2, it's best to use externalServices block instead of attempting to mock HttpResponse. That way you don't need to attempt and mock the internals of Ktor, and it's not complicated at all.
externalServices {
hosts("https://your-fake-host") {
routing {
get("/api/v1/something/{id}/") {
call.respondText(
"{}",
contentType = ContentType.Application.Json,
status = HttpStatusCode.OK
)
}
}
}
}
This need to be wrapped with testApplication

Ktor Serializer for class 'Response' is not found

Plugin and dependeny:
id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version"
implementation "io.ktor:ktor-serialization:$ktor_version"
Application file:
fun main() {
embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
install(ContentNegotiation) {
json()
}
userRouter()
}.start(wait = true)
}
UserRouter:
fun Application.userRouter() {
routing {
get("/users/{id}") {
val id = call.parameters["id"]?.toInt() ?: -1
val user = User("Sam", "sam#gmail.com", "abc123")
val response = if (id == 1) {
Response("hahaha", false)
} else {
Response(user, true) //< - here, use String type will work
}
call.respond(response)
}
}
}
User:
#Serializable
data class User(
val name: String,
val email: String,
val password: String
)
Response:
#Serializable
data class Response<T>(
val data: T,
val success: Boolean
)
Logs:
2021-12-02 18:04:34.214 [eventLoopGroupProxy-4-1] ERROR ktor.application - Unhandled: GET - /users/7
kotlinx.serialization.SerializationException: Serializer for class 'Response' is not found.
Mark the class as #Serializable or provide the serializer explicitly.
at kotlinx.serialization.internal.Platform_commonKt.serializerNotRegistered(Platform.common.kt:91)
at kotlinx.serialization.SerializersKt__SerializersKt.serializer(Serializers.kt:155)
Any help would be appreciated.
The problem is that type for the response variable is Response<out Any> (the lowest common denominator between String and User types is Any) and the serialization framework cannot serialize the Any type.
That's werid, the following works, but to me they are just the same:
get("/users/{id}") {
val id = call.parameters["id"]?.toInt() ?: -1
val user = User("Sam", "sam#gmail.com", "abc123")
/* val response = if (id == 1) { //<- determine response here won't work, why?
Response("hahaha", false)
} else {
Response(user, true)
}
call.respond(response)*/
if (id == 1) {
call.respond(Response("hahaha", false))
} else {
call.respond(Response(user, true))
}
}
You should give a type to a generic when you set a nullable generic to null, otherwise, the plugin doesn't know how to serialize it.
for eg.
//define
#Serializable
data class Response<T>(
val data: T?,
val success: Boolean
)
///usage:
val response = Response<String>(null, false)

How to ignore empty database result for the first time and wait for server result in application?

My app using room as a database and retrofit as a network calling api.
i am observing database only as a single source of truth. every thing is working fine. But i am not finding solution of one scenario.
Like for the first time when user open app it do following operations
fetch data from db
fetch data from server
because currently database is empty so it sends empty result to observer which hide progress bar . i want to discard that event and send result to observer when server dump data to database. even server result is empty. so progress bar should always hide once their is confirmation no data exists.
in other words application should always rely on database but if it empty then it should wait until server response and then notify observer.
this is my code
observer
viewModel.characters.observe(viewLifecycleOwner, Observer {
Log.e("status is ", "${it.message} at ${System.currentTimeMillis()}")
when (it.status) {
Resource.Status.SUCCESS -> {
binding.progressBar.visibility = View.GONE
if (!it.data.isNullOrEmpty()) adapter.setItems(ArrayList(it.data))
}
Resource.Status.ERROR -> {
Toast.makeText(requireContext(), it.message, Toast.LENGTH_SHORT).show()
binding.progressBar.visibility = View.GONE
}
Resource.Status.LOADING ->
binding.progressBar.visibility = View.VISIBLE
}
})
ViewModel
#HiltViewModel
class CharactersViewModel #Inject constructor(
private val repository: CharacterRepository
) : ViewModel() {
val characters = repository.getCharacters()
}
Repository
class CharacterRepository #Inject constructor(
private val remoteDataSource: CharacterRemoteDataSource,
private val localDataSource: CharacterDao
) {
fun getCharacters() : LiveData<Resource<List<Character>>> {
return performGetOperation(
databaseQuery = { localDataSource.getAllCharacters() },
networkCall = { remoteDataSource.getCharacters() },
saveCallResult = { localDataSource.insertAll(it.results) }
)
}
}
Utility function for all api and database handling
fun <T, A> performGetOperation(databaseQuery: () -> LiveData<T>,
countQuery: () -> Int,
networkCall: suspend () -> Resource<A>,
saveCallResult: suspend (A) -> Unit): LiveData<Resource<T>> =
liveData(Dispatchers.IO) {
emit(Resource.loading())
val source = databaseQuery().map { Resource.success(it,"database") }.distinctUntilChanged()
emitSource(source)
val responseStatus = networkCall()
if (responseStatus.status == SUCCESS) {
saveCallResult(responseStatus.data!!)
} else if (responseStatus.status == ERROR) {
emit(Resource.error(responseStatus.message!!))
}
}
LocalDataSource
#Dao
interface CharacterDao {
#Query("SELECT * FROM characters")
fun getAllCharacters() : LiveData<List<Character>>
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(characters: List<Character>)
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(character: Character)
}
DataSource
class CharacterRemoteDataSource #Inject constructor(
private val characterService: CharacterService
): BaseDataSource() {
suspend fun getCharacters() = getResult { characterService.getAllCharacters() }}
}
Base Data Source
abstract class BaseDataSource {
protected suspend fun <T> getResult(call: suspend () -> Response<T>): Resource<T> {
try {
Log.e("status is", "started")
val response = call()
if (response.isSuccessful) {
val body = response.body()
if (body != null) return Resource.success(body,"server")
}
return error(" ${response.code()} ${response.message()}")
} catch (e: Exception) {
return error(e.message ?: e.toString())
}
}
private fun <T> error(message: String): Resource<T> {
Timber.d(message)
return Resource.error("Network call has failed for a following reason: $message")
}
}
Character Service
interface CharacterService {
#GET("character")
suspend fun getAllCharacters() : Response<CharacterList>
}
Resource
data class Resource<out T>(val status: Status, val data: T?, val message: String?) {
enum class Status {
SUCCESS,
ERROR,
LOADING
}
companion object {
fun <T> success(data: T,message : String): Resource<T> {
return Resource(Status.SUCCESS, data, message)
}
fun <T> error(message: String, data: T? = null): Resource<T> {
return Resource(Status.ERROR, data, message)
}
fun <T> loading(data: T? = null): Resource<T> {
return Resource(Status.LOADING, data, "loading")
}
}
}
CharacterList
data class CharacterList(
val info: Info,
val results: List<Character>
)
What is the best way by that i ignore database if it is empty and wait for server response and then notify observer

How to get lambda/function signature in Kotlin?

I have developed the code below:
import com.google.gson.Gson
import com.rabbitmq.client.Delivery
typealias MessageHandler<T> = (payload: T, args: HashMap<String, String>) -> Unit
class MessageRouter {
private var handlers: HashMap<String, Function<Unit>> = hashMapOf()
fun <T> bind(routingKey: String, handler: MessageHandler<T>) : MessageRouter {
handlers[routingKey] = handler
return this
}
fun consume(message: Delivery) {
if(message.envelope.routingKey in handlers) {
val currentHandler = handlers[message.envelope.routingKey]
if(message.properties.contentType == "application/json") {
///// TODO: Get function signature!
val gson = Gson()
//// TODO: Deserialize data and call the handler function
}
}
}
}
To be used as follows:
val messageRouter = MessageRouter()
val messageRouter = MessageRouter()
messageRouter.bind(EventSubscriberConstants.REGISTRATION_ROUTING_KEY) { user: UserDTO, _ -> register(user) }
basicConsume(
queueName, true,
{ _, message ->
messageRouter.consume(message)
}, null, null
)
I need the input lambda signature to extract UserDTO class and use Gson to deserialize it and process. I can not find any way to get function signature in Kotlin!

How can I access header in a service?

I'm trying to handle JWT-authentication in gRPC on my backend. I can extract the JWT in an interceptor but how do I access it in my service? I think it should be done with a CoroutineContextServerInterceptor but this doesn't work:
val jwtKey: Context.Key<String> = Context.key("jwtKey")
fun main() {
ServerBuilder.forPort(8980).intercept(UserInjector).addService(MyService).build().start().awaitTermination()
}
object UserInjector : CoroutineContextServerInterceptor() {
override fun coroutineContext(call: ServerCall<*, *>, headers: Metadata): CoroutineContext {
val jwtString = headers.get(Metadata.Key.of("jwt", Metadata.ASCII_STRING_MARSHALLER))
println("coroutineContext: $jwtString")
return GrpcContextElement(Context.current().withValue(jwtKey, jwtString))
}
}
object MyService : MyServiceGrpcKt.MyServiceCoroutineImplBase() {
override suspend fun testingJWT(request: Test.MyRequest): Test.MyResponse {
println("testingJWT: ${jwtKey.get()}")
return Test.MyResponse.getDefaultInstance()
}
}
Output:
coroutineContext: something
testingJWT: null
I think you'll need to propagate that in its own coroutine context element.
class JwtElement(val jwtString: String) : CoroutineContext.Element {
companion object Key : CoroutineContext.Key<JwtElement>
override val key: CoroutineContext.Key<JwtElement>
get() = Key
}
object UserInjector : CoroutineContextServerInterceptor() {
override fun coroutineContext(call: ServerCall<*, *>, headers: Metadata): CoroutineContext {
val jwtString = headers.get(Metadata.Key.of("jwt", Metadata.ASCII_STRING_MARSHALLER))
println("coroutineContext: $jwtString")
return JwtElement(jwtString)
}
}
object MyService : MyServiceGrpcKt.MyServiceCoroutineImplBase() {
override suspend fun testingJWT(request: Test.MyRequest): Test.MyResponse {
println("testingJWT: ${coroutineContext[JwtElement]}")
return Test.MyResponse.getDefaultInstance()
}
}