I've a Ktor server application using the Resources plugin for type-safe routing. Now I want to create a custom plugin to validate the resource instance.
But I can't figure what is the correct phase for intercepting the pipeline.
A custom "Validation Phase" inserted before the call phase seems to be executed to early as the Resource instance is not yet added to the ApplicationCall attributes. I do not really understand why thats the case, because the decoding of the Resource instance should by done in the Plugins phase? Found the following in io.ktor.server.resources.Routing:
public fun <T : Any> Route.handle(
serializer: KSerializer<T>,
body: suspend PipelineContext<Unit, ApplicationCall>.(T) -> Unit
) {
intercept(ApplicationCallPipeline.Plugins) {
val resources = application.plugin(Resources)
try {
val resource = resources.resourcesFormat.decodeFromParameters(serializer, call.parameters)
call.attributes.put(ResourceInstanceKey, resource)
} catch (cause: Throwable) {
throw BadRequestException("Can't transform call to resource", cause)
}
}
...
}
If I add my custom validation phase after the call phase it's executed to late, after the route handler.
Here some example code...
Route and Resource:
fun Route.exampleRouting() {
get<ExampleResource> { example ->
println("Validated value: ${example.somevalue}")
call.respond(HttpStatusCode.OK)
}
}
fun Application.registerExampleRoutes() {
routing {
exampleRouting()
}
}
#Serializable
#Resource("/example")
class ExampleResource(val somevalue: String)
Custom validation plugin:
val ResourcesValidation = createApplicationPlugin("ResourcesValidation") {
on(ValidationHook) { call ->
val resourceInstanceKey =
call.attributes.allKeys.filterIsInstance<AttributeKey<Any>>().find { it.name == "ResourceInstance" }
// PROBLEM: resourceInstanceKey is null here, ResourceInstance not yet added to call attributes
resourceInstanceKey?.let {
val resourceInstance = call.attributes[resourceInstanceKey]
// Validate resource instance here...
println("validated")
}
}
}
object ValidationHook : Hook<suspend (ApplicationCall) -> Unit> {
val ValidationPhase: PipelinePhase = PipelinePhase("Validation")
override fun install(
pipeline: ApplicationCallPipeline,
handler: suspend (ApplicationCall) -> Unit
) {
pipeline.insertPhaseBefore(ApplicationCallPipeline.Call, ValidationPhase)
pipeline.intercept(ValidationPhase) { handler(call) }
}
}
And of cause installing the plugin and registering the routes in the Application:
fun Application.module() {
...
install(ResourcesValidation)
...
registerExampleRoutes()
...
}
I've tried the same with the Base API but same result.
So..is there any way to intercept the pipeline at the right time to validate the Resource instance before the route handler is executed?
To solve your problem you can write a RouteScopedPlugin and install it into the routing because a resource instance is put into the call attributes while interception of a route's call pipeline, not an application's pipeline.
fun main() {
embeddedServer(Netty, port = 4444) {
install(Resources)
routing {
install(ResourcesValidation)
get<ExampleResource> { example ->
println("Validated value: ${example.somevalue}")
call.respond(HttpStatusCode.OK)
}
}
}.start(wait = true)
}
val ResourcesValidation = createRouteScopedPlugin("ResourcesValidation") {
on(ValidationHook) { call ->
try {
val resourceInstance = call.attributes[AttributeKey("ResourceInstance")]
println("validated")
} catch (_: IllegalStateException) {
// attribute not found
}
}
}
object ValidationHook : Hook<suspend (ApplicationCall) -> Unit> {
val ValidationPhase: PipelinePhase = PipelinePhase("Validation")
override fun install(
pipeline: ApplicationCallPipeline,
handler: suspend (ApplicationCall) -> Unit
) {
pipeline.insertPhaseAfter(ApplicationCallPipeline.Plugins, ValidationPhase)
pipeline.intercept(ValidationPhase) { handler(call) }
}
}
Related
I am currently working on an api for my client application which needs to process http requests (using unirest) asynchronously as of now. I am new to CompletableFuture and haven't worked with anything similar up to this point. I was wondering whether the following structure makes sense:
// Request.kt (simplified)
class Request<T>(
// other variables relevant to the request such as body or path ...
private val responseType: Class<T>
) {
fun prepareRequest(action: (HttpRequest<*>) -> U): U {
// preprocesses the request, adds body if necessary and returns the request itself
}
fun executeAsync(action: (HttpResponse<T>) -> Unit) {
prepareRequest { req ->
action(req.asObjectAsync(responseType).get()) // Unirest call that (still) freezes the UI
}
}
// Builder logic ...
}
// ApiClient.kt (simplified)
abstract class ApiClient {
protected fun <T> executeAsync(req: Request<T>, action: (T) -> Unit) {
req.executeAsync { res ->
if (res.isSuccess){
action(res.body)
} else {
throw RuntimeException("res != 200")
}
}
}
}
// AuthClient.kt (simplified)
class AuthClient : ApiClient() {
fun signin(email: String, password: String, onSuccess: () -> Unit) {
executeAsync(
Request.builder(TokenModel::class.java)
.post("/signin")
.body(SignInModel(email, password))
.build()
) {
onSuccess() // this is going to refresh the UI, once the http request has been executed
}
}
}
As the call to get on CompletableFuture freezes the UI I thought of including an Executor or a Thread instead so that executeAsync in Request becomes the following:
fun executeAsync(action: (HttpResponse<T>) -> Unit) {
prepareRequest { req ->
Executors.newSingleThreadScheduledExecutor().execute {
action(it.asObjectAsync(responseType).get())
}
}
}
Is my structure overly complex or does it have to be like that? Do I need the Thread/Executor or can this be achieved in a different way?
I am using Retrofit with OkHttp. I like to add okhttp3.Interceptor to log the annotation path.
For example:
#GET("posts/{post_id}")
suspend fun getPost(#Path("post_id") postId: String): Response<Post>
Interceptor
class MyInterceptor : Interceptor {
#Throws(IOException::class)
override fun intercept(chain: Chain): Response {
// log the path here
}
}
I like to log posts/{post_id} each time getPost is called.
How can it be achieved?
found the answer in the tags of the request
fun extractPath(request: Request) = request.run {
javaClass.getDeclaredField("tags").let {
it.isAccessible = true
#Suppress("UNCHECKED_CAST")
it.get(this) as Map<Class<*>, Any>
}
}.run {
values.first().run {
javaClass.getDeclaredField("method").let {
it.isAccessible = true
(it.get(this) as Method).annotations[0]
}
}
}.run {
when (this) {
is DELETE -> value
is GET -> value
is HEAD -> value
is OPTIONS -> value
is PATCH -> value
is POST -> value
is PUT -> value
else -> null
}
}
You may want to check out HttpLoggingInterceptor, for example here.
I am testing an API written in Kotlin using the KTOR framework. For the testing, I am using JUnit5 and Mockito. There is a route class where a route is defined which I need to test. Here is the route class :-
fun Application.configureRouting() {
routing {
post("/someRoute") {
val service = MyService()
val request: JsonNode = call.receive()
launch {
service.dummyFunction(request)
}
val mapper = ObjectMapper()
val responseStr = "{\"status\":\"success\",\"message\":\"Request has been received successfully\"}"
val response: JsonNode = mapper.readTree(responseStr)
call.fireHttpResponse(HttpStatusCode.OK, response)
}
}
}
This is the test case I am writing for it :-
class RouteTest {
#Mock
var service = MyService()
// read the configuration properties
private val testEnv = createTestEnvironment {
config = HoconApplicationConfig(ConfigFactory.load("application.conf"))
}
#Before
fun setUp() = withApplication(testEnv) {
MockitoAnnotations.openMocks(MyService::class)
}
#Test
fun test() = withApplication(testEnv) {
withTestApplication(Application::configureRouting) {
runBlocking {
Mockito.`when`(service.dummyFunction(Mockito.any()).thenReturn(true)
with(handleRequest(HttpMethod.Post, "/someRoute") {
setBody("some body")
}) {
assertEquals(HttpStatusCode.OK, response.status())
}
}
}
}
}
When I run the test, it calls the actual "dummyFunction()" method instead of the mocked one and hence, it is failing. Am I doing something wrong?
Because your service in test is different from the service you mocked. To solve this, you need to inject the service into your class, or pass the service as an argument.
Read more: IoC, DI.
The simplest way to solve your problem is to define the service parameter for the configureRouting method and pass a corresponding argument in the test and production code when calling it.
fun Application.configureRouting(service: MyService) {
routing {
post("/someRoute") {
val request: JsonNode = call.receive()
launch {
service.dummyFunction(request)
}
val mapper = ObjectMapper()
val responseStr = "{\"status\":\"success\",\"message\":\"Request has been received successfully\"}"
val response: JsonNode = mapper.readTree(responseStr)
call.fireHttpResponse(HttpStatusCode.OK, response)
}
}
}
class RouteTest {
#Mock
var service = MyService()
private val testEnv = createTestEnvironment {
config = HoconApplicationConfig(ConfigFactory.load("application.conf"))
}
#Test
fun test() = withApplication(testEnv) {
withTestApplication({ configureRouting(service) }) {
runBlocking {
// Your test...
}
}
I am new to kotlin and jetpack, I am requested to handle errors (exceptions) coming from the PagingData, I am not allowed to use Flow, I am only allowed to use LiveData.
This is the Repository:
class GitRepoRepository(private val service: GitRepoApi) {
fun getListData(): LiveData<PagingData<GitRepo>> {
return Pager(
// Configuring how data is loaded by adding additional properties to PagingConfig
config = PagingConfig(
pageSize = 20,
enablePlaceholders = false
),
pagingSourceFactory = {
// Here we are calling the load function of the paging source which is returning a LoadResult
GitRepoPagingSource(service)
}
).liveData
}
}
This is the ViewModel:
class GitRepoViewModel(private val repository: GitRepoRepository) : ViewModel() {
private val _gitReposList = MutableLiveData<PagingData<GitRepo>>()
suspend fun getAllGitRepos(): LiveData<PagingData<GitRepo>> {
val response = repository.getListData().cachedIn(viewModelScope)
_gitReposList.value = response.value
return response
}
}
In the Activity I am doing:
lifecycleScope.launch {
gitRepoViewModel.getAllGitRepos().observe(this#PagingActivity, {
recyclerViewAdapter.submitData(lifecycle, it)
})
}
And this is the Resource class which I created to handle exceptions (please provide me a better one if there is)
data class Resource<out T>(val status: Status, val data: T?, val message: String?) {
companion object {
fun <T> success(data: T?): Resource<T> {
return Resource(Status.SUCCESS, data, null)
}
fun <T> error(msg: String, data: T?): Resource<T> {
return Resource(Status.ERROR, data, msg)
}
fun <T> loading(data: T?): Resource<T> {
return Resource(Status.LOADING, data, null)
}
}
}
As you can see I am using Coroutines and LiveData. I want to be able to return the exception when it occurs from the Repository or the ViewModel to the Activity in order to display the exception or a message based on the exception in a TextView.
Your GitRepoPagingSource should catch retryable errors and pass them forward to Paging as a LoadResult.Error(exception).
class GitRepoPagingSource(..): PagingSource<..>() {
...
override suspend fun load(..): ... {
try {
... // Logic to load data
} catch (retryableError: IOException) {
return LoadResult.Error(retryableError)
}
}
}
This gets exposed to the presenter-side of Paging as LoadState, which can be reacted to via LoadStateAdapter, .addLoadStateListener, etc as well as .retry. All of the presenter APIs from Paging expose these methods, such as PagingDataAdapter: https://developer.android.com/reference/kotlin/androidx/paging/PagingDataAdapter
You gotta pass your error handler to the PagingSource
class MyPagingSource(
private val api: MyApi,
private val onError: (Throwable) -> Unit,
): PagingSource<Int, MyModel>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, YourModel> {
try {
...
} catch(e: Exception) {
onError(e) // <-- pass your error listener here
}
}
}
Currently I write a small demo-app which uses Ktor as its Application Environment and Kodein as the Dependency Injection Framework.
During the initialization of the Application I do import some modules, one of those I would like to replace during the initialization of the Integration Tests:
fun Application.module(testing: Boolean = false) {
logger.debug { "Starting main" }
restModule()
di {
bind<Json>() with singleton {
Json {
...
}
}
import(persistenceModule)
}
In the test, I would like to use a different persistenceModule, say eg. a MemoryModule. My tests are initialized like:
fun start() {
val configPath = ClassLoader.getSystemResource("application-acceptanceTest.conf").file
engine = embeddedServer(CIO, commandLineEnvironment(arrayOf("-config=$configPath")))
engine.start()
val disposable = engine.environment.monitor.subscribe(ApplicationStarted) { application: Application ->
started = true
}
while (!started) {
Thread.sleep(10)
}
disposable.dispose()
}
I have tried already to call
engine.application.di
but this gives me (quite obviously) only access to the Ktor Feature, which is already initialized. Is anything like this possible at all?
Kodein-DI allows you to override dependencies. Regarding the following interface:
interface Repository {
fun save()
fun find()
}
You can have a production implementation, included in its own DI module:
class PersistenceRepository : Repository {
/* implementation */
}
val persistenceModule = DI.Module("persistenceModule") {
bind<Repository>() with singleton { PersistenceRepository() }
}
and also a test implementation of that same interface:
class MemoryRepository : Repository {
/* implementation */
}
val memoryModule = DI.Module("memoryModule") {
bind<Repository>(overrides = true) with singleton { MemoryRepository() }
}
Note the overrides parameter that needs to be explicit.
You can pass a DI container to your Ktor function:
val mainDI = DI {
import(persistenceModule)
}
fun Application.main(di: DI) {
di { extend(di) }
}
And extend the mainDI in your tests, to override the proper bindings with the memoryModule:
class ApplicationTest {
val testDI = DI {
extend(mainDI)
import(memoryModule, allowOverride = true)
}
#Test
fun myTest() {
withTestApplication({ main(testDI) })
// ...
}
}