Skip to content

Alternative Ktor Raise DSL #3581

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

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ internal fun ApplicationCall.errorResponse(errors: NonEmptyList<RequestError>):
public class RaiseErrorResponseConfig(
public var errorResponse: ErrorResponse = ::defaultErrorsResponse,
) {
public fun onErrorRespond(response: (NonEmptyList<RequestError>) -> Response) {
public fun errorResponse(response: (NonEmptyList<RequestError>) -> Response) {
errorResponse = response
}
}
Expand All @@ -42,7 +42,6 @@ public val RaiseErrorResponse: RouteScopedPlugin<RaiseErrorResponseConfig> = cre
}
}

// <editor-fold desc="Default Response">
private fun defaultErrorsResponse(errors: NonEmptyList<RequestError>): Response =
Response.Companion.raw(
TextContent(
Expand All @@ -51,4 +50,3 @@ private fun defaultErrorsResponse(errors: NonEmptyList<RequestError>): Response
status = BadRequest,
),
)
// </editor-fold>
Original file line number Diff line number Diff line change
@@ -1,54 +1,84 @@
package arrow.raise.ktor.server

import arrow.core.nel
import arrow.core.raise.ExperimentalRaiseAccumulateApi
import arrow.core.raise.Raise
import arrow.core.raise.RaiseAccumulate
import arrow.raise.ktor.server.request.Parameter
import arrow.raise.ktor.server.request.ParameterDelegateProvider
import arrow.raise.ktor.server.request.ParameterTransform
import arrow.raise.ktor.server.request.RaisingParameterProvider
import arrow.raise.ktor.server.request.RequestError
import arrow.raise.ktor.server.request.parameterOrRaise
import arrow.raise.ktor.server.request.pathOrRaise
import arrow.raise.ktor.server.request.queryOrRaise
import arrow.raise.ktor.server.request.receiveNullableOrRaise
import arrow.raise.ktor.server.request.receiveOrRaise
import io.ktor.http.*
import io.ktor.server.routing.*
import io.ktor.util.reflect.typeInfo
import kotlinx.io.files.Path
import kotlin.jvm.JvmName

public class RaiseRoutingContext(
private val raise: Raise<Response>,
private val routingContext: RoutingContext,
) : Raise<Response> by raise {

routingContext: RoutingContext,
) : CallRaiseContext(routingContext.call), Raise<Response> by raise {
@PublishedApi
internal val errorRaise: Raise<RequestError> =
object : Raise<RequestError> {
override fun raise(requestError: RequestError): Nothing = raise(call.errorResponse(requestError.nel()))
override fun raise(requestError: RequestError) = raise(call.errorResponse(requestError.nel()))
}

public val call: RoutingCall get() = routingContext.call
public fun raise(requestError: RequestError): Nothing = errorRaise.raise(requestError)

public val pathRaising: ParameterDelegateProvider =
object : RaisingParameterProvider(errorRaise, call.pathParameters) {
override fun parameter(name: String) = Parameter.Path(name)
}
public val pathRaising: RaisingParameterProvider = call.pathParameters.delegate(Parameter::Path)
public val queryRaising: RaisingParameterProvider = call.queryParameters.delegate(Parameter::Query)
public suspend fun formParametersDelegate(): RaisingParameterProvider =
errorRaise.receiveOrRaise<Parameters>(call).delegate(Parameter::Form)

public val queryRaising: ParameterDelegateProvider =
object : RaisingParameterProvider(errorRaise, call.queryParameters) {
override fun parameter(name: String) = Parameter.Query(name)
}
private fun Parameters.delegate(parameter: (String) -> Parameter) = RaisingParameterProvider(errorRaise, this, parameter)
}

/** Temporary intersection type, until we have context parameters */
public open class CallRaiseContext internal constructor(public val call: RoutingCall) {
// <editor-fold desc="raising extensions">
@JvmName("pathOrRaiseReified")
public inline fun <reified A : Any> Raise<RequestError>.pathOrRaise(name: String): A = pathOrRaise<A>(call, name)
public inline fun <A : Any> Raise<RequestError>.pathOrRaise(name: String, transform: Raise<String>.(String) -> A): A = pathOrRaise(call, name, transform)
public inline fun <A : Any> Raise<RequestError>.pathOrRaise(name: String, transform: ParameterTransform<A>): A = pathOrRaise(call, name, transform)
public fun Raise<RequestError>.pathOrRaise(name: String): String = pathOrRaise(call, name)

@JvmName("queryOrRaiseReified")
public inline fun <reified A : Any> Raise<RequestError>.queryOrRaise(name: String): A = queryOrRaise<A>(call, name)
public inline fun <A : Any> Raise<RequestError>.queryOrRaise(name: String, transform: Raise<String>.(String) -> A): A = queryOrRaise(call, name, transform)
public inline fun <A : Any> Raise<RequestError>.queryOrRaise(name: String, transform: ParameterTransform<A>): A = queryOrRaise(call, name, transform)
public fun Raise<RequestError>.queryOrRaise(name: String): String = queryOrRaise(call, name)

public suspend inline fun <reified A : Any> Raise<RequestError>.receiveOrRaise(): A = receiveOrRaise(call)
public suspend inline fun <reified A : Any> Raise<RequestError>.receiveNullableOrRaise(): A? = receiveNullableOrRaise(call)
// </editor-fold>

// <editor-fold desc="accumulating extensions">
@ExperimentalRaiseAccumulateApi
@JvmName("pathOrAccumulateReified")
public inline fun <reified A : Any> RaiseAccumulate<RequestError>.pathOrAccumulate(name: String): RaiseAccumulate.Value<A> = accumulating { pathOrRaise<A>(call, name) }

@ExperimentalRaiseAccumulateApi
public inline fun <A : Any> RaiseAccumulate<RequestError>.pathOrAccumulate(name: String, transform: ParameterTransform<A>): RaiseAccumulate.Value<A> = accumulating { pathOrRaise(call, name, transform) }

@ExperimentalRaiseAccumulateApi
public fun RaiseAccumulate<RequestError>.pathOrAccumulate(name: String): RaiseAccumulate.Value<String> = accumulating { pathOrRaise(call, name) }

@ExperimentalRaiseAccumulateApi
@JvmName("queryOrAccumulateReified")
public inline fun <reified A : Any> RaiseAccumulate<RequestError>.queryOrAccumulate(name: String): RaiseAccumulate.Value<A> = accumulating { queryOrRaise<A>(call, name) }

@ExperimentalRaiseAccumulateApi
public inline fun <A : Any> RaiseAccumulate<RequestError>.queryOrAccumulate(name: String, transform: ParameterTransform<A>): RaiseAccumulate.Value<A> = accumulating { queryOrRaise(call, name, transform) }

@ExperimentalRaiseAccumulateApi
public fun RaiseAccumulate<RequestError>.queryOrAccumulate(name: String): RaiseAccumulate.Value<String> = accumulating { queryOrRaise(call, name) }


@ExperimentalRaiseAccumulateApi
public suspend inline fun <reified A : Any> RaiseAccumulate<RequestError>.receiveOrAccumulate(): RaiseAccumulate.Value<A> = accumulating { receiveOrRaise(call) }

@ExperimentalRaiseAccumulateApi
public suspend inline fun <reified A : Any> RaiseAccumulate<RequestError>.receiveNullableOrAccumulate(): RaiseAccumulate.Value<A?> = accumulating { receiveNullableOrRaise(call) }
// </editor-fold>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package arrow.raise.ktor.server.request

import arrow.core.raise.ExperimentalRaiseAccumulateApi
import arrow.core.raise.Raise
import arrow.core.raise.RaiseAccumulate
import io.ktor.http.*
import io.ktor.util.reflect.*
import kotlin.jvm.JvmName
import kotlin.properties.PropertyDelegateProvider
import kotlin.reflect.KProperty

@OptIn(ExperimentalRaiseAccumulateApi::class)
public abstract class AccumulatingParameterProvider internal constructor(
private val accumulate: RaiseAccumulate<RequestError>,
private val parameters: Parameters,
) {
protected abstract fun parameter(name: String): Parameter

public companion object {
public inline operator fun invoke(accumulate: RaiseAccumulate<RequestError>, parameters: Parameters, crossinline parameter: (String) -> Parameter): AccumulatingParameterProvider =
object : AccumulatingParameterProvider(accumulate, parameters) {
override fun parameter(name: String): Parameter = parameter(name)
}
}

@PublishedApi
internal fun getString(name: String): RaiseAccumulate.Value<String> =
accumulate.accumulating { parameterOrRaise(parameters, parameter(name)) }

@PublishedApi
internal fun <O : Any> getReified(name: String, typeInfo: TypeInfo): RaiseAccumulate.Value<O> =
accumulate.accumulating { parameterOrRaise(parameters, parameter(name), typeInfo) }

@PublishedApi
internal fun <O : Any> getTransformed(name: String, transform: ParameterTransform<O>): RaiseAccumulate.Value<O> =
accumulate.accumulating { parameterOrRaise(parameters, parameter(name), transform) }

// type-bound delegate providers
public operator fun invoke(): Provider<RaiseAccumulate.Value<String>> =
Provider { _, prop -> getString(prop.name) }

@PublishedApi
internal fun <O : Any> invoke(typeInfo: TypeInfo): Provider<RaiseAccumulate.Value<O>> =
Provider { _, prop -> getReified(prop.name, typeInfo) }

public operator fun <O : Any> invoke(transform: ParameterTransform<O>): Provider<RaiseAccumulate.Value<O>> =
Provider { _, prop -> getTransformed(prop.name, transform) }

@JvmName("invokeReified")
public inline operator fun <reified O : Any> invoke(): Provider<RaiseAccumulate.Value<O>> = invoke(typeInfo<O>())

// name-bound delegate providers
public operator fun invoke(name: String): RaiseAccumulate.Value<String> =
getString(name)

public operator fun <O : Any> invoke(name: String, transform: ParameterTransform<O>): RaiseAccumulate.Value<O> =
getTransformed(name, transform)

@JvmName("invokeReified")
public inline operator fun <reified O : Any> invoke(name: String): RaiseAccumulate.Value<O> =
getReified(name, typeInfo<O>())

// unbound delegate
public inline operator fun <reified O : Any> provideDelegate(
thisRef: Nothing?,
prop: KProperty<*>,
): RaiseAccumulate.Value<O> = getReified(prop.name, typeInfo<O>())
}

internal typealias Provider<T> = PropertyDelegateProvider<Nothing?, T>
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@file:OptIn(ExperimentalContracts::class)

package arrow.raise.ktor.server.request

import arrow.core.NonEmptyList
Expand All @@ -8,23 +10,21 @@ import arrow.core.raise.recover
import arrow.raise.ktor.server.RaiseRoutingContext
import arrow.raise.ktor.server.Response
import arrow.raise.ktor.server.errorResponse
import io.ktor.http.*
import io.ktor.server.routing.*
import io.ktor.util.reflect.*
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind.AT_MOST_ONCE
import kotlin.contracts.contract

public class CallValidationContext @PublishedApi internal constructor(
@PublishedApi
internal val call: RoutingCall,
raise: Raise<NonEmptyList<RequestError>>,
) : RaiseAccumulate<RequestError>(raise) {
public val pathAccumulating: ParameterDelegateProvider =
object : AccumulatingParameterProvider(this, call.pathParameters) {
override fun parameter(name: String) = Parameter.Path(name)
}

public val queryAccumulating: ParameterDelegateProvider =
object : AccumulatingParameterProvider(this, call.queryParameters) {
override fun parameter(name: String) = Parameter.Query(name)
}
public val pathAccumulating: AccumulatingParameterProvider = call.pathParameters.delegate(Parameter::Path)
public val queryAccumulating: AccumulatingParameterProvider = call.queryParameters.delegate(Parameter::Query)
public suspend fun formParametersDelegate(): AccumulatingParameterProvider = receiveOrRaise<Parameters>(call).delegate(Parameter::Form)

@ExperimentalRaiseAccumulateApi
public suspend inline fun <reified A : Any> receiveAccumulating(): Value<A> =
Expand All @@ -33,20 +33,33 @@ public class CallValidationContext @PublishedApi internal constructor(
@ExperimentalRaiseAccumulateApi
public suspend inline fun <reified A : Any> receiveNullableAccumulating(): Value<A?> =
accumulating { receiveNullableOrRaise(call, typeInfo<A>()) }

private inline fun Parameters.delegate(crossinline parameter: (String) -> Parameter) =
AccumulatingParameterProvider(this@CallValidationContext, this, parameter)
}

@ExperimentalRaiseAccumulateApi
public inline fun <A> RaiseRoutingContext.validate(
transform: (errors: NonEmptyList<RequestError>) -> Response = call::errorResponse,
block: CallValidationContext.() -> A,
): A = call.validate(block) { raise(transform(it)) }
): A {
contract {
callsInPlace(block, AT_MOST_ONCE)
callsInPlace(transform, AT_MOST_ONCE)
}
return call.validate({ raise(transform(it)) }, block)
}

@ExperimentalRaiseAccumulateApi
public inline fun <A> RoutingCall.validate(
block: CallValidationContext.() -> A,
recover: (errors: NonEmptyList<RequestError>) -> A,
): A =
recover({
block(CallValidationContext(this@validate, this))
block: CallValidationContext.() -> A,
): A {
contract {
callsInPlace(block, AT_MOST_ONCE)
callsInPlace(recover, AT_MOST_ONCE)
}
return recover({
block(CallValidationContext(this@validate, this@recover))
}, recover)

}

This file was deleted.

Loading
Loading