Skip to content

Commit

Permalink
feat: body, redirect and handle supports
Browse files Browse the repository at this point in the history
  • Loading branch information
programadorthi committed Aug 6, 2024
1 parent 72cbefe commit f90cf27
Show file tree
Hide file tree
Showing 7 changed files with 586 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ internal class RedirectApplicationCall(
override val name: String = "",
override val uri: String = "",
private val previousCall: ApplicationCall,
private val newMethod: RouteMethod? = null,
parameters: Parameters,
) : ApplicationCall, CoroutineScope {
override val application: Application get() = previousCall.application

override val attributes: Attributes get() = previousCall.attributes

override val routeMethod: RouteMethod get() = previousCall.routeMethod
override val routeMethod: RouteMethod get() = newMethod ?: previousCall.routeMethod

override val parameters: Parameters by lazy(LazyThreadSafetyMode.NONE) {
Parameters.build {
Expand Down
144 changes: 113 additions & 31 deletions core/common/src/dev/programadorthi/routing/core/Routing.kt
Original file line number Diff line number Diff line change
Expand Up @@ -64,24 +64,41 @@ public class Routing internal constructor(
name: String,
lookUpOnParent: Boolean = false,
): Boolean {
return when {
!lookUpOnParent -> namedRoutes.containsKey(name)
else ->
generateSequence(seed = this) { it.parent?.asRouting }
.firstOrNull { it.namedRoutes.containsKey(name) } != null
}
return getRouteByName(name = name, lookUpOnParent = lookUpOnParent) != null
}

public fun canHandleByName(
name: String,
method: RouteMethod,
lookUpOnParent: Boolean = false,
): Boolean {
val route = getRouteByName(name = name, lookUpOnParent = lookUpOnParent) ?: return false
return checkByMethod(method, route)
}

public fun canHandleByPath(
path: String,
lookUpOnParent: Boolean = false,
): Boolean {
return when {
!lookUpOnParent -> canHandleByPath(path = path, routing = this)
else ->
generateSequence(seed = this) { it.parent?.asRouting }
.firstOrNull { canHandleByPath(path = path, routing = it) } != null
}
return getRouteByPath(
path = path,
method = null,
lookUpOnParent = lookUpOnParent,
) != null
}

public fun canHandleByPath(
path: String,
method: RouteMethod,
lookUpOnParent: Boolean = false,
): Boolean {
val route =
getRouteByPath(
path = path,
method = method,
lookUpOnParent = lookUpOnParent,
) ?: return false
return checkByMethod(method, route)
}

public fun execute(call: ApplicationCall) {
Expand Down Expand Up @@ -135,32 +152,75 @@ public class Routing internal constructor(
name: String,
route: Route,
) {
check(!namedRoutes.containsKey(name)) {
"Duplicated named route. Found '$name' to ${namedRoutes[name]} and $route"
val registered = getRouteByName(name = name, lookUpOnParent = false)
check(registered == null) {
"Duplicated named route. Found '$name' to $registered and $route"
}
namedRoutes[name] = route
}

private fun canHandleByPath(
path: String,
routing: Routing,
private fun checkByMethod(
method: RouteMethod,
route: Route,
): Boolean {
var routingChildren = routing.children
var hasHandle = false
val selector = RouteMethodRouteSelector(method)
return route.selector == selector ||
route.children.any { it.selector == selector }
}

private fun getRouteByName(
name: String,
lookUpOnParent: Boolean,
): Route? =
when {
!lookUpOnParent -> namedRoutes[name]
else ->
generateSequence(seed = this) { it.parent?.asRouting }
.mapNotNull { it.namedRoutes[name] }
.firstOrNull()
}

private fun getRouteByPath(
path: String,
method: RouteMethod?,
lookUpOnParent: Boolean = false,
): Route? =
when {
!lookUpOnParent -> getRouteByPath(path = path, routing = this, method = method)
else ->
generateSequence(seed = this) { it.parent?.asRouting }
.mapNotNull { getRouteByPath(path = path, routing = it, method = method) }
.firstOrNull()
}

private fun getRouteByPath(
path: String,
routing: Routing,
method: RouteMethod?,
): Route? {
// Creating a fake Tree starting from the Routing selector
val fakeRoute = Route(parent = null, selector = routing.selector)
fakeRoute.createRouteFromPath(path)

generateSequence(seed = fakeRoute.children) { it.firstOrNull()?.children }
.flatten()
.forEach { child ->
val found =
routingChildren.firstOrNull { it.selector == child.selector } ?: return false
hasHandle = found.handlers.isNotEmpty()
routingChildren = found.children
}
val leaf = fakeRoute.createRouteFromPath(path)
if (method != null) {
leaf.createChild(RouteMethodRouteSelector(method))
}

return hasHandle
// On a fake Tree each deeper Route have only one child
var node = fakeRoute.children.firstOrNull()
var nextLevel = routing.children
var leafRoute: Route
while (true) {
// Time to compare level by level until leaf Route
leafRoute = nextLevel.firstOrNull { it.selector == node?.selector } ?: return null
// Time to go to the next level or stop
node = node?.children?.firstOrNull() ?: break
nextLevel = leafRoute.children
}

return when {
leafRoute.handlers.isNotEmpty() -> leafRoute
else -> null
}
}

private fun removeChild(route: Route) {
Expand All @@ -186,7 +246,7 @@ public class Routing internal constructor(
pathParameters: Parameters,
): String {
val namedRoute =
namedRoutes[name]
getRouteByName(name = name, lookUpOnParent = false)
?: throw RouteNotFoundException(message = "Named route not found with name: $name")
val routeSelectors = namedRoute.allSelectors()
val skipPathParameters =
Expand Down Expand Up @@ -446,6 +506,28 @@ public fun Routing.call(
)
}

@HiddenFromObjC
public fun <T : Any> Routing.callWithBody(
name: String = "",
uri: String = "",
routeMethod: RouteMethod = RouteMethod.Empty,
attributes: Attributes = Attributes(),
parameters: Parameters = Parameters.Empty,
body: T,
) {
execute(
ApplicationCall(
application = application,
body = body,
name = name,
uri = uri,
routeMethod = routeMethod,
attributes = attributes,
parameters = parameters,
),
)
}

@HiddenFromObjC
@KtorDsl
public fun routing(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ import io.ktor.http.Parameters
import io.ktor.util.AttributeKey
import io.ktor.util.Attributes
import io.ktor.util.pipeline.execute
import io.ktor.util.reflect.TypeInfo
import kotlinx.coroutines.launch

private val RECEIVE_TYPE_KEY: AttributeKey<TypeInfo> = AttributeKey("ReceiveType")
private val RECEIVE_TYPE: AttributeKey<Any> = AttributeKey("KotlinRoutingReceiveType")

/**
* A single act of communication between a client and server.
Expand Down Expand Up @@ -57,14 +56,27 @@ public interface ApplicationCall {
public fun ApplicationCall.path(): String = uri.substringBefore('?')

/**
* The [TypeInfo] recorded from the last [call.receive<Type>()] call.
* Get the body sent to the current [ApplicationCall]
*
* @return A no null value of the type requested
* @throws IllegalStateException when the current body is null
*/
public var ApplicationCall.receiveType: TypeInfo
get() = attributes[RECEIVE_TYPE_KEY]
internal set(value) {
attributes.put(RECEIVE_TYPE_KEY, value)
public fun <T> ApplicationCall.receive(): T =
checkNotNull(receiveNullable<T>()) {
"There is no body to receive in the call: $this"
}

/**
* Get the body sent to the current [ApplicationCall] or null instead
*
* @return The instance whether exists or null when missing
*/
@Suppress("UNCHECKED_CAST")
public fun <T> ApplicationCall.receiveNullable(): T? {
val body = attributes.getOrNull(RECEIVE_TYPE)
return body as? T
}

public fun ApplicationCall(
application: Application,
name: String = "",
Expand All @@ -86,24 +98,47 @@ public fun ApplicationCall(
)
}

public fun <T : Any> ApplicationCall(
application: Application,
body: T,
name: String = "",
uri: String = "",
routeMethod: RouteMethod = RouteMethod.Empty,
attributes: Attributes = Attributes(),
parameters: Parameters = Parameters.Empty,
): ApplicationCall {
attributes.put(RECEIVE_TYPE, body)
return ApplicationCall(
application = application,
name = name,
routeMethod = routeMethod,
uri = uri,
attributes = attributes,
parameters = parameters,
)
}

public fun ApplicationCall.redirectToName(
name: String,
parameters: Parameters = Parameters.Empty,
method: RouteMethod? = null,
) {
redirect(path = "", name = name, parameters = parameters)
redirect(path = "", name = name, parameters = parameters, routeMethod = method)
}

public fun ApplicationCall.redirectToPath(
path: String,
parameters: Parameters = Parameters.Empty,
method: RouteMethod? = null,
) {
redirect(path = path, name = "", parameters = parameters)
redirect(path = path, name = "", parameters = parameters, routeMethod = method)
}

private fun ApplicationCall.redirect(
path: String,
name: String,
parameters: Parameters,
routeMethod: RouteMethod?,
) {
with(application) {
launch {
Expand All @@ -114,6 +149,7 @@ private fun ApplicationCall.redirect(
uri = path,
coroutineContext = coroutineContext,
parameters = parameters,
newMethod = routeMethod,
),
)
}
Expand Down
Loading

0 comments on commit f90cf27

Please sign in to comment.