kroegerama / openapi-kgen

Generate modern API Clients in Kotlin from OpenAPI specifications. Supports OpenAPI >= 3.0.0.
Apache License 2.0
22 stars 4 forks source link

Proposal for a new user facing API #2

Open robin-thoni opened 3 years ago

robin-thoni commented 3 years ago

I'm proposing a new API for users to simplify error management. The current generated API lets the user handle all the errors by himself, like as following (written on the fly, might not be 100 % correct):

var response: Response<MyObject> = null
var errorString: String = null
try {
    response = DefaultApi.Companion::getSomeResources()
} catch (e: Exception) {
    errorString = e.localizedMessage
}
if (response?.isSuccessful) {
    showData(response?.body())
} else {
    val moshi = Moshi.Builder().build()
    val jsonAdapter = moshi.adapter<ErrorObject>(type)
    val errorString = response?.errorBody()?.string()
    val errorObject = if (errorString != null) {
        jsonAdapter.fromJson(errorString)
    } else {
        null
    }
    if (errorObject != null) {
        errorString = errorObject.message
    }
    else {
        errorString = getString(R.string.error_unknown)
    }
    showError(errorString)
}

That makes a lot of error management to repeat each time.

I'm proposing to add another layer of generated API that would be equivalent to this (this code works):

import com.squareup.moshi.Moshi
import retrofit2.Response
import java.lang.Exception

fun <T, TError> Response<T>.errorObject(type: Class<TError>): TError? {
    val moshi = Moshi.Builder().build()
    val jsonAdapter = moshi.adapter<TError>(type)
    val errorString = errorBody()?.string()
    return if (errorString != null) {
        jsonAdapter.fromJson(errorString)
    } else {
        null
    }
}

fun <T, TError> Response<T>.handle(type: Class<TError>): Triple<Response<T>, T?, TError?> {

    return if (isSuccessful) {
        Triple<Response<T>, T?, TError?>(this, body(), null)

    } else {
        Triple<Response<T>, T?, TError?>(this, null, errorObject(type))
    }
}

suspend fun <TResponse, TError> exec(type: Class<TError>, f: suspend () -> Response<TResponse>): Triple<Response<TResponse>?, TResponse?, Pair<TError?, Exception?>?> {
    return try {
        val (response, result, error) = f.invoke().handle(type)
        Triple(response, result, if (error == null) null else Pair(error, null))
    } catch (e: Exception) {
        Triple(null, null, Pair(null, e))
    }
}

suspend fun <TResponse, TError> (suspend () -> Response<TResponse>).execute(type: Class<TError>): Triple<Response<TResponse>?, TResponse?, Pair<TError?, Exception?>?> {
    return exec(type, this)
}

suspend fun <TResponse, TError, T1> (suspend (T1) -> Response<TResponse>).execute(type: Class<TError>, arg1: T1): Triple<Response<TResponse>?, TResponse?, Pair<TError?, Exception?>?> {
    return exec(type) {
        invoke(arg1)
    }
}

And the usage is as follow:

val (response, result, error) = DefaultApi.Companion::getSomeResources.execute(ErrorHolder::class.java, null)
if (error != null) {
    val errorMessage = error.first?.error?.message ?: error.second?.localizedMessage ?: getString(R.string.error_unknown)
    Log.e("MainActivity", "FAILED: $errorMessage")
} else {
    Log.d("MainActivity", "SUCCESS: " + (result ?: "null"))
}

A few drawback of this externally implemented way:

Any thought ?

PS: Kudos for this project, it's really awesome!

kroegerama commented 3 years ago

Thanks for your detailed feedback! I was indeed planning to add some error handling like your proposal. The problem is that retrofit by itself does not support typed error responses by itself. And OpenAPI does in fact support multiple response types. Also, I want to keep the API of the generated code as "lowlevel" as possible.

I'm currently using these helpers in my production apps: Android Kaiteki. But these do not support custom error types either. You can use them like this in a ViewModel:

    private val someListing = MutableLiveData(
        viewModelScope.retrofitListing { MyApi.getSomething(paramOne) }
    )
    val devices : LiveData<List<Something>> = devicesListing.result()
    val running = LiveData<Boolean> = devicesListing.isRunning()

Or just use the wrapped response:

val response = withProgress {
    retrofitCall { MyApi.deleteMe(myId) }
}
response.success {
    viewModel.refresh()
}.noSuccessOrError {
    snackBar(...)
}

But I am planning to create a companion library for kgen with a few helper functions. This library should use the moshi instance of the ApiHolder, if possible. My plan ist to make that library platform independent (i.e. not use android LiveData). I'm currently experimenting with the new SharedFlow API and will for sure add the possibility to add custom error types. Maybe this should also support multiple error types. E.g:

PS: Kudos for this project, it's really awesome!

Thanks! I'm using this generator for many projects already and wanted to share it with the world so anyone can use it :)