Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Response return type #85

Closed
vovahost opened this issue Oct 27, 2022 · 7 comments
Closed

Add Response return type #85

vovahost opened this issue Oct 27, 2022 · 7 comments

Comments

@vovahost
Copy link

vovahost commented Oct 27, 2022

Is your feature request related to a problem? Please describe.
Add support for Response return type similar to Retrofit.

Additional context
Currently the supported types are:

interface StarWarsApi {

    @GET("people/{id}/")
    suspend fun getPerson(@Path("id") personId: Int): Person

    @GET("people")
    fun getPeopleFlow(@Query("page") page: Int): Flow<Person>

    @GET("people/{id}/")
    fun getPersonCall(@Path("id") personId: Int): Call<Person>

}

We also need this use case:

@GET("people/{id}/")
fun getPersonCall(@Path("id") personId: Int): Response<Person>

This will allow us to check the status code and other parameters similar to Retrofit Response:
https://github.com/square/retrofit/blob/fbf1225e28e2094bec35f587b8933748b705d167/retrofit/src/main/java/retrofit2/Response.java

We can also copy these tests: https://github.com/square/retrofit/blob/fbf1225e28e2094bec35f587b8933748b705d167/retrofit/src/test/java/retrofit2/ResponseTest.java

Additional Benefits
Make it easier to migrate from Retrofit by reusing existing Response<DataType> return values and Response interface api.

@vovahost
Copy link
Author

Something like this:

/**
 * Converter to enable the use of Response<> as return type
 */
class DefaultResponseConverter : SuspendResponseConverter {

  override fun supportedType(typeData: TypeData, isSuspend: Boolean): Boolean {
    return typeData.qualifiedName == "app/codepreview/plugin/data/network/converter/Response.kt"
  }

  override suspend fun <RequestType> wrapSuspendResponse(
    typeData: TypeData,
    requestFunction: suspend () -> Pair<TypeInfo, HttpResponse>,
    ktorfit: Ktorfit,
  ): Any {
    return ktorfit.httpClient.async {
      try {
        val (typeInfo, rawResponse) = requestFunction()

        val code: Int = rawResponse.status.value
        when {
          code < 200 || code >= 300 -> {
            val errorBody = rawResponse.body<Any>()
            Response.error<RequestType>(errorBody, rawResponse)
          }

          code == 204 || code == 205 -> {
            Response.success<RequestType>(null, rawResponse)
          }

          else -> {
            val body = rawResponse.body<RequestType>(typeInfo)
            Response.success(body, rawResponse)
          }
        }
      } catch (exception: Exception) {
        throw exception
      }
    }
  }
}


/** An HTTP response.  */
@Suppress("MemberVisibilityCanBePrivate")
class Response<T> private constructor(
  rawResponse: HttpResponse,
  body: T?,
  errorBody: Any?,
) {
  private val rawResponse: HttpResponse
  private val body: T?
  private val errorBody: Any?

  init {
    this.rawResponse = rawResponse
    this.body = body
    this.errorBody = errorBody
  }

  /** The raw response from the HTTP client.  */
  fun raw(): HttpResponse {
    return rawResponse
  }

  /** HTTP status.  */
  val status: HttpStatusCode get() = rawResponse.status

  /** HTTP status code.  */
  val code: Int
    get() = status.value

  /** HTTP status message or null if unknown.  */
  val message: String
    get() = status.toString()

  /** HTTP headers.  */
  val headers: Headers
    get() = rawResponse.headers

  /** Returns true if status code is in the range [200..300).  */
  val isSuccessful: Boolean
    get() = status.isSuccess()

  /** The deserialized response body of a [successful][.isSuccessful] response.  */
  fun body(): T? {
    return body
  }

  /** The raw response body of an [unsuccessful][.isSuccessful] response.  */
  fun errorBody(): Any? {
    return errorBody
  }

  override fun toString(): String {
    return rawResponse.toString()
  }

  companion object {

    /**
     * Create a successful response from `rawResponse` with `body` as the deserialized
     * body.
     */
    fun <T> success(body: T?, rawResponse: HttpResponse): Response<T?> {
      require(rawResponse.status.isSuccess()) { "rawResponse must be successful response" }
      return Response(rawResponse, body, null)
    }

    /** Create an error response from `rawResponse` with `body` as the error body.  */
    fun <T> error(body: Any, rawResponse: HttpResponse): Response<T?> {
      require(!rawResponse.status.isSuccess()) { "rawResponse should not be successful response" }
      return Response(rawResponse, null, body)
    }
  }
}

@DatL4g
Copy link
Contributor

DatL4g commented Oct 27, 2022

This is already possible.

Look #23 and #19, use a SuspendResponseConverter like you did in your example.
If you need more information look at my implementation https://github.com/hadiyarajesh/flower

@vovahost
Copy link
Author

vovahost commented Nov 2, 2022

@DatL4g, for some reason requestFunction() is throwing an error:

java.lang.ClassCastException: class kotlinx.coroutines.DeferredCoroutine cannot be cast to 
class com.example.CustomResponse (kotlinx.coroutines.DeferredCoroutine 
and com.example.CustomResponse are in unnamed module of loader 'app')

inside:

  override suspend fun <RequestType> wrapSuspendResponse(
    typeData: TypeData,
    requestFunction: suspend () -> Pair<TypeInfo, HttpResponse>,
    ktorfit: Ktorfit,
  ): Any {
    return ktorfit.httpClient.async {
      try {
        val (typeInfo, rawResponse) = requestFunction()
       // return CustomResponse
     }

@DatL4g
Copy link
Contributor

DatL4g commented Nov 2, 2022

@vovahost because you're not doing it correctly.

The converter has to look like this:

override suspend fun <RequestType> wrapSuspendResponse(
	typeData: TypeData,
    requestFunction: suspend () -> Pair<TypeInfo, HttpResponse>,
    ktorfit: Ktorfit
): Any {
	return try {
		// the requestFunction will handle the request itself, no need to call ktorfit.httpClient
		val (info, response) = requestFunction()
		Response.success(response.body(info))
	} catch (e: Throwable) {
		Response.error(e)
	}
}

BTW @Foso any reason we pass the Ktorfit instance here? Seems to be a little confusing

@vovahost
Copy link
Author

vovahost commented Nov 4, 2022

@DatL4g You were right. What you suggested and your repo helped. Thank you.

@CMingTseng
Copy link

This is already possible.

Look #23 and #19, use a SuspendResponseConverter like you did in your example. If you need more information look at my implementation https://github.com/hadiyarajesh/flower

so the demo & Response with Ktorfit can merge into Ktorfit release ?

THX @Foso

@Foso
Copy link
Owner

Foso commented May 27, 2023

Sorry for the very late Answer and thank you for the contribution. With 1.4.0 the Response class is now added

@Foso Foso closed this as completed May 27, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants