Skip to content

Commit

Permalink
Merge pull request #343 from Runnect/feature/test-apiresult-call-adpater
Browse files Browse the repository at this point in the history
[Refactor] ViewModel에서 Flow를 사용할 수 있도록 수정
  • Loading branch information
dongx0915 authored Mar 30, 2024
2 parents 6b9b913 + de702f1 commit 6a5991d
Show file tree
Hide file tree
Showing 16 changed files with 405 additions and 33 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.runnect.runnect.data.dto.response.base

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class ErrorResponse<T>(
@SerialName("status")
val status: Int,
@SerialName("success")
val success: Boolean?,
@SerialName("message")
val message: String?,
@SerialName("error")
val error: String?,
@SerialName("data")
val data: T? = null
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.runnect.runnect.data.network.calladapter

import com.google.gson.Gson
import com.runnect.runnect.data.dto.response.base.ErrorResponse
import com.runnect.runnect.domain.common.RunnectException
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import okhttp3.Request
import okio.Timeout

class ResultCall<T>(private val call: Call<T>) : Call<Result<T>> {

private val gson = Gson()

override fun execute(): Response<Result<T>> {
throw UnsupportedOperationException("ResultCall doesn't support execute")
}

override fun enqueue(callback: Callback<Result<T>>) {
call.enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
val apiResult = if (response.isSuccessful) {
response.body()?.let {
Result.success(it)
} ?: Result.failure(
RunnectException(
code = response.code(),
message = ERROR_MSG_RESPONSE_IS_NULL
)
)
} else {
Result.failure(parseErrorResponse(response))
}

callback.onResponse(
this@ResultCall,
Response.success(apiResult)
)
}

override fun onFailure(call: Call<T>, t: Throwable) {
callback.onFailure(this@ResultCall, t)
}
})
}

private fun parseErrorResponse(response: Response<*>): RunnectException {
val errorJson = response.errorBody()?.string()

return runCatching {
val errorBody = gson.fromJson(errorJson, ErrorResponse::class.java)
val message = errorBody?.run {
message ?: error ?: ERROR_MSG_COMMON
}

RunnectException(
code = errorBody.status,
message = message
)
}.getOrElse {
RunnectException(
code = response.code(),
message = ERROR_MSG_COMMON
)
}
}

override fun clone(): Call<Result<T>> = ResultCall(call.clone())
override fun isExecuted(): Boolean = call.isExecuted
override fun cancel() = call.cancel()
override fun isCanceled(): Boolean = call.isCanceled
override fun request(): Request = call.request()
override fun timeout(): Timeout = call.timeout()

companion object {
private const val ERROR_MSG_COMMON = "알 수 없는 에러가 발생하였습니다."
private const val ERROR_MSG_RESPONSE_IS_NULL = "데이터를 불러올 수 없습니다."
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.runnect.runnect.data.network.calladapter

import retrofit2.Call
import retrofit2.CallAdapter
import java.lang.reflect.Type

class ResultCallAdapter<T>(
private val responseType: Type
) : CallAdapter<T, Call<Result<T>>> {

override fun responseType() = responseType

// Retrofit의 Call을 Result<>로 변환
override fun adapt(call: Call<T>): Call<Result<T>> {
return ResultCall(call)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.runnect.runnect.data.network.calladapter

import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.Retrofit
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type

class ResultCallAdapterFactory private constructor() : CallAdapter.Factory() {

override fun get(
returnType: Type,
annotations: Array<Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
// 최상위 타입이 Call인지 체크(suspend로 선언시 Call로 감싸짐)
if (getRawType(returnType) != Call::class.java) {
return null
}

check(returnType is ParameterizedType) {
"Call return type must be parameterized as Call<Foo> or Call<out Foo>"
}

val responseType = getParameterUpperBound(0, returnType)
if (getRawType(responseType) != Result::class.java) {
return null
}

check(responseType is ParameterizedType) {
"ApiResult return type must be parameterized as ApiResult<Foo> or ApiResult<out Foo>"
}

return ResultCallAdapter<Any>(
getParameterUpperBound(
0,
responseType
)
)
}

companion object {
@JvmStatic
fun create() = ResultCallAdapterFactory()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.runnect.runnect.data.network.interceptor

import com.google.gson.Gson
import com.google.gson.JsonParseException
import com.google.gson.JsonSyntaxException
import com.runnect.runnect.data.dto.response.base.BaseResponse
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import timber.log.Timber

/**
* BaseResponse에서 data만 추출 (불필요한 래핑 제거)
* - 서버에서 내려준 형식이 아니라면 응답 그대로 반환
*/
class ResponseInterceptor : Interceptor {

private val gson = Gson()

override fun intercept(chain: Interceptor.Chain): Response {
val originalResponse = chain.proceed(chain.request())
if (!originalResponse.isSuccessful) return originalResponse

val bodyString = originalResponse.peekBody(Long.MAX_VALUE).string()
val newResponseBodyString = jsonToBaseResponse(bodyString)?.let {
it.toResponseBody("application/json".toMediaTypeOrNull())
} ?: return originalResponse

return originalResponse.newBuilder()
.code(originalResponse.code)
.body(newResponseBodyString)
.build()
.apply {
Timber.v("""\n
origin = ${originalResponse.peekBody(Long.MAX_VALUE).string()}
new = ${this.peekBody(Long.MAX_VALUE).string()}
""".trimIndent()
)
}
}

private fun jsonToBaseResponse(body: String): String? {
return try {
val baseResponse = gson.fromJson(body, BaseResponse::class.java)
gson.toJson(baseResponse.data)
} catch (e: JsonSyntaxException) {
null // JSON 구문 분석 오류 발생 시 원래 형식을 반환
} catch (e: JsonParseException) {
null // JSON 파싱 오류 발생 시 원래 형식을 반환
} catch (e: Exception) {
null // 기타 예외 발생 시 원래 형식을 반환
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.runnect.runnect.data.network

import com.runnect.runnect.domain.common.RunnectException
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow

fun <R, D> Result<R>.mapToFlowResult(
mapper: (R) -> D
): Flow<Result<D>> = flow {
val result = when {
this@mapToFlowResult.isSuccess -> Result.success(
// CallAdapter에서 body가 null인 경우도 걸러주고 있으므로
// Result.success의 데이터가 null인 경우는 없을듯함
mapper(this@mapToFlowResult.getOrNull()!!)
)

else -> Result.failure(
this@mapToFlowResult.exceptionOrNull() ?: RunnectException()
)
}

emit(result)
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,27 @@ import com.runnect.runnect.data.dto.response.ResponsePostMyHistory
import com.runnect.runnect.data.dto.response.ResponsePutMyDrawCourse
import com.runnect.runnect.data.dto.response.ResponsePostDiscoverUpload
import com.runnect.runnect.data.dto.response.ResponsePostScrap
import com.runnect.runnect.data.network.mapToFlowResult
import com.runnect.runnect.data.source.remote.RemoteCourseDataSource
import com.runnect.runnect.domain.entity.DiscoverSearchCourse
import com.runnect.runnect.domain.entity.DiscoverMultiViewItem.*
import com.runnect.runnect.domain.entity.DiscoverUploadCourse
import com.runnect.runnect.domain.entity.EditableCourseDetail
import com.runnect.runnect.domain.entity.RecommendCoursePagingData
import com.runnect.runnect.domain.repository.CourseRepository
import kotlinx.coroutines.flow.Flow
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.Response
import javax.inject.Inject

class CourseRepositoryImpl @Inject constructor(private val remoteCourseDataSource: RemoteCourseDataSource) :
CourseRepository {
override suspend fun getMarathonCourse(): Result<List<MarathonCourse>?> = runCatching {
remoteCourseDataSource.getMarathonCourse().data?.toMarathonCourses()

override suspend fun getMarathonCourse(): Flow<Result<List<MarathonCourse>>> {
return remoteCourseDataSource.getMarathonCourse().mapToFlowResult {
it.toMarathonCourses()
}
}

override suspend fun getRecommendCourse(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import com.runnect.runnect.data.dto.request.RequestPostPublicCourse
import com.runnect.runnect.data.dto.request.RequestPostRunningHistory
import com.runnect.runnect.data.dto.request.RequestPutMyDrawCourse
import com.runnect.runnect.data.dto.response.ResponseGetCourseDetail
import com.runnect.runnect.data.dto.response.ResponseGetDiscoverMarathon
import com.runnect.runnect.data.dto.response.ResponseGetDiscoverRecommend
import com.runnect.runnect.data.dto.response.ResponseGetDiscoverSearch
import com.runnect.runnect.data.dto.response.ResponseGetDiscoverUploadCourse
Expand All @@ -26,8 +25,6 @@ import retrofit2.Response
import retrofit2.http.*

interface CourseService {
@GET("/api/public-course/marathon")
suspend fun getMarathonCourse(): BaseResponse<ResponseGetDiscoverMarathon>

@GET("/api/public-course")
suspend fun getRecommendCourse(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.runnect.runnect.data.service

import com.runnect.runnect.data.dto.response.ResponseGetDiscoverMarathon
import retrofit2.http.GET

interface CourseV2Service {
@GET("/api/public-course/marathon")
suspend fun getMarathonCourse(): Result<ResponseGetDiscoverMarathon>
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,17 @@ import com.runnect.runnect.data.dto.response.ResponsePatchPublicCourse
import com.runnect.runnect.data.dto.response.ResponsePostScrap
import com.runnect.runnect.data.dto.response.base.BaseResponse
import com.runnect.runnect.data.service.CourseService
import com.runnect.runnect.data.service.CourseV2Service
import okhttp3.MultipartBody
import okhttp3.RequestBody
import javax.inject.Inject

class RemoteCourseDataSource @Inject constructor(
private var courseV2Service: CourseV2Service,
private val courseService: CourseService
) {
suspend fun getMarathonCourse(): BaseResponse<ResponseGetDiscoverMarathon> =
courseService.getMarathonCourse()
suspend fun getMarathonCourse(): Result<ResponseGetDiscoverMarathon> =
courseV2Service.getMarathonCourse()

suspend fun getRecommendCourse(
pageNo: String,
Expand Down
Loading

0 comments on commit 6a5991d

Please sign in to comment.