Skip to content

Commit

Permalink
Merge pull request #28 from outfoxx/task/fix-sonar
Browse files Browse the repository at this point in the history
Fix sonar smells
  • Loading branch information
kdubb authored Dec 7, 2022
2 parents 7715b52 + 7a3e99e commit 16f66fd
Show file tree
Hide file tree
Showing 4 changed files with 213 additions and 333 deletions.
166 changes: 164 additions & 2 deletions core/src/main/kotlin/io/outfoxx/sunday/RequestFactory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,43 @@ import io.outfoxx.sunday.http.Parameters
import io.outfoxx.sunday.http.Request
import io.outfoxx.sunday.http.Response
import io.outfoxx.sunday.http.ResultResponse
import io.outfoxx.sunday.http.contentLength
import io.outfoxx.sunday.http.contentType
import io.outfoxx.sunday.mediatypes.codecs.MediaTypeDecoders
import io.outfoxx.sunday.mediatypes.codecs.MediaTypeEncoders
import io.outfoxx.sunday.mediatypes.codecs.StructuredMediaTypeDecoder
import io.outfoxx.sunday.mediatypes.codecs.TextMediaTypeDecoder
import io.outfoxx.sunday.mediatypes.codecs.decode
import io.outfoxx.sunday.problems.NonStandardStatus
import io.outfoxx.sunday.utils.from
import kotlinx.coroutines.flow.Flow
import okio.BufferedSource
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.zalando.problem.DefaultProblem
import org.zalando.problem.Problem
import org.zalando.problem.Status
import org.zalando.problem.StatusType
import org.zalando.problem.ThrowableProblem
import java.io.Closeable
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.full.createType
import kotlin.reflect.typeOf

/**
* Factory for requests, responses and event sources.
*/
abstract class RequestFactory : Closeable {

companion object {

private val logger = LoggerFactory.getLogger(RequestFactory::class.java)

private val failureStatusCodes = 400 until 600
private val emptyDataStatusCodes = setOf(204, 205)
}

/**
* Purpose of the request.
*/
Expand Down Expand Up @@ -316,7 +336,7 @@ abstract class RequestFactory : Closeable {
*
* @return [ResultResponse] returned from the generated request.
*/
abstract suspend fun <B : Any, R : Any> resultResponse(
suspend fun <B : Any, R : Any> resultResponse(
method: Method,
pathTemplate: String,
pathParameters: Parameters? = null,
Expand All @@ -326,7 +346,36 @@ abstract class RequestFactory : Closeable {
acceptTypes: List<MediaType>? = null,
headers: Parameters? = null,
resultType: KType
): ResultResponse<R>
): ResultResponse<R> {

val response =
response(
method,
pathTemplate,
pathParameters,
queryParameters,
body,
contentTypes,
acceptTypes,
headers
)

if (isFailureResponse(response)) {
logger.trace("Parsing failure response")

throw parseFailure(response)
}

logger.trace("Parsing success response")

return ResultResponse(
parseSuccess(
response,
resultType,
),
response
)
}

/**
* Creates an [EventSource] that uses the provided request parameters to supply
Expand Down Expand Up @@ -466,4 +515,117 @@ abstract class RequestFactory : Closeable {
): Flow<D>

abstract fun close(cancelOutstandingRequests: Boolean)

private fun isFailureResponse(response: Response) =
failureStatusCodes.contains(response.statusCode)

private fun <T : Any> parseSuccess(response: Response, resultType: KType): T {

val body = response.body
if (emptyDataStatusCodes.contains(response.statusCode)) {
if (resultType != typeOf<Unit>()) {
throw SundayError(SundayError.Reason.UnexpectedEmptyResponse)
}
@Suppress("UNCHECKED_CAST")
return Unit as T
}

if (body == null || response.contentLength == 0L) {
throw SundayError(SundayError.Reason.NoData)
}

val contentType =
response.contentType?.let { MediaType.from(it.toString()) }
?: throw SundayError(
SundayError.Reason.InvalidContentType,
response.contentType?.value ?: ""
)

val contentTypeDecoder = mediaTypeDecoders.find(contentType)
?: throw SundayError(SundayError.Reason.NoDecoder, contentType.value)

try {

return contentTypeDecoder.decode(body, resultType)
} catch (x: Throwable) {
throw SundayError(SundayError.Reason.ResponseDecodingFailed, cause = x)
}
}

private fun parseFailure(response: Response): ThrowableProblem {

val status =
try {
Status.valueOf(response.statusCode)
} catch (ignored: IllegalArgumentException) {
NonStandardStatus(response)
}

return parseFailureResponseBody(response, status)
}

private fun parseFailureResponseBody(response: Response, status: StatusType): ThrowableProblem {

val body = response.body

return if (body != null && response.contentLength != 0L) {

val contentType =
response.contentType?.let { MediaType.from(it.toString()) }
?: MediaType.OctetStream

if (!contentType.compatible(MediaType.Problem)) {
parseUnknownFailureResponseBody(contentType, body, status)
} else {
parseProblemResponseBody(body)
}
} else {
Problem.valueOf(status)
}
}

private fun parseProblemResponseBody(body: BufferedSource): ThrowableProblem {
val problemDecoder =
mediaTypeDecoders.find(MediaType.Problem)
?: throw SundayError(SundayError.Reason.NoDecoder, MediaType.Problem.value)

problemDecoder as? StructuredMediaTypeDecoder
?: throw SundayError(
SundayError.Reason.NoDecoder,
"'${MediaType.Problem}' decoder must support structured decoding"
)

val decoded: Map<String, Any> = problemDecoder.decode(body)

val problemType = decoded["type"]?.toString() ?: ""
val problemClass = (registeredProblemTypes[problemType] ?: DefaultProblem::class).createType()

return problemDecoder.decode(decoded, problemClass)
}

private fun parseUnknownFailureResponseBody(
contentType: MediaType,
body: BufferedSource,
status: StatusType
): ThrowableProblem {
val (responseText, responseData) =
if (contentType.compatible(MediaType.AnyText))
body.readString(Charsets.from(contentType)) to null
else
null to body.readByteArray()

return Problem.builder()
.withStatus(status)
.withTitle(status.reasonPhrase)
.apply {
if (responseText != null) {
with("responseText", responseText)
}
if (responseData != null) {
with("responseData", responseData)
}
}
.build()
}

}
Loading

0 comments on commit 16f66fd

Please sign in to comment.