Created
November 11, 2023 11:00
-
-
Save rundis/90a1f414f0ff2f77bd72b61c47235dd6 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package no.routehandling.utils | |
import arrow.core.Either | |
import arrow.core.flatMap | |
import arrow.core.raise.either | |
import arrow.core.raise.ensureNotNull | |
import io.ktor.http.* | |
import io.ktor.server.application.* | |
import io.ktor.server.request.* | |
import io.ktor.server.response.* | |
import kotlinx.serialization.Serializable | |
import kotliquery.Session | |
import kotliquery.TransactionalSession | |
import kotliquery.sessionOf | |
import kotliquery.using | |
import org.slf4j.LoggerFactory | |
import javax.sql.DataSource | |
// A naive/simple representation of errors you typically handle in your handlers | |
@Serializable | |
sealed class RouteError { | |
@Serializable | |
object Unauthorized : RouteError() | |
@Serializable | |
object Forbidden : RouteError() | |
@Serializable | |
data class BadRequest(val message: String) : RouteError() | |
@Serializable | |
data class NotFound(val message: String) : RouteError() | |
data class ServerError(val error: Throwable) : RouteError() | |
fun toStatusCode() = when (this) { | |
is BadRequest -> HttpStatusCode.BadRequest | |
is NotFound -> HttpStatusCode.NotFound | |
is ServerError -> HttpStatusCode.InternalServerError | |
Unauthorized -> HttpStatusCode.Unauthorized | |
Forbidden -> HttpStatusCode.Forbidden | |
} | |
} | |
// Either friendly DB convenience functions | |
object DBUtils { | |
fun <T> withSession(ds: DataSource, block: (session: Session) -> T): Either<RouteError.ServerError, T> = | |
Either.catch { | |
using(sessionOf(ds)) { session -> block(session) } | |
}.mapLeft { RouteError.ServerError(it) } | |
fun <T> withTx(ds: DataSource, block: (tx: TransactionalSession) -> T): Either<RouteError.ServerError, T> = | |
Either.catch { | |
using(sessionOf(ds)) { session -> session.transaction(block) } | |
}.mapLeft { RouteError.ServerError(it) } | |
} | |
// Application extensions | |
fun ApplicationCall.requestParamInt(name: String): Either<RouteError, Int> = either { | |
ensureNotNull(parameters[name]?.toIntOrNull()) { | |
RouteError.BadRequest("Parameter $name is required and must be an Int") | |
} | |
} | |
suspend inline fun <reified T : Any> ApplicationCall.respondEither( | |
res: Either<RouteError, T>, | |
status: HttpStatusCode = HttpStatusCode.OK, | |
) { | |
when (res) { | |
is Either.Left -> { | |
when (val err = res.value) { | |
is RouteError.BadRequest -> | |
respond(HttpStatusCode.BadRequest, err) | |
is RouteError.NotFound -> | |
respond(HttpStatusCode.NotFound, err) | |
is RouteError.ServerError -> { | |
val log = LoggerFactory.getLogger("ResponseError") | |
log.error("Uncaught server error", err.error) | |
respond(HttpStatusCode.InternalServerError) | |
} | |
else -> | |
respond(res.value.toStatusCode()) | |
} | |
} | |
is Either.Right -> { | |
val stuff = res.value | |
respond(status, stuff) | |
} | |
} | |
} | |
suspend inline fun <reified T> ApplicationCall.bodyObject(): Either<RouteError, T> { | |
return Either.catch { | |
receiveNullable<T>() | |
}.mapLeft { | |
RouteError.BadRequest("Failed to retrieve object of class: ${T::class.java}") | |
}.flatMap { | |
if (it == null) { | |
Either.Left(RouteError.BadRequest("No object found in request body for class: ${T::class.java}")) | |
} else { | |
Either.Right(it) | |
} | |
} | |
} | |
// Route related Either extensions | |
fun <T> Either<RouteError, T?>.mapNotNull(message: String): Either<RouteError, T> = this.flatMap { | |
if (it == null) { | |
Either.Left(RouteError.NotFound(message)) | |
} else { | |
Either.Right(it) | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment