Skip to content

Instantly share code, notes, and snippets.

@rundis
Created November 11, 2023 11:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rundis/90a1f414f0ff2f77bd72b61c47235dd6 to your computer and use it in GitHub Desktop.
Save rundis/90a1f414f0ff2f77bd72b61c47235dd6 to your computer and use it in GitHub Desktop.
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