-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
d33a49e
commit 0799c6c
Showing
8 changed files
with
625 additions
and
33 deletions.
There are no files selected for viewing
This file contains 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
79 changes: 79 additions & 0 deletions
79
voyager/common/src/dev/programadorthi/routing/voyager/VoyagerResources.kt
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
/* | ||
* Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. | ||
*/ | ||
|
||
package dev.programadorthi.routing.voyager | ||
|
||
import dev.programadorthi.routing.core.application.Application | ||
import dev.programadorthi.routing.core.application.BaseApplicationPlugin | ||
import dev.programadorthi.routing.core.application.plugin | ||
import io.ktor.http.URLBuilder | ||
import io.ktor.resources.href | ||
import io.ktor.util.AttributeKey | ||
import io.ktor.resources.Resources as ResourcesCore | ||
|
||
/** | ||
* Adds support for type-safe routing using [ResourcesCore]. | ||
* | ||
* Example: | ||
* ```kotlin | ||
* @Resource("/users") | ||
* class Users { | ||
* @Resource("/{id}") | ||
* class ById(val parent: Users = Users(), val id: Long) | ||
* | ||
* @Resource("/add") | ||
* class Add(val parent: Users = Users(), val name: String) | ||
* } | ||
* | ||
* routing { | ||
* get<Users.ById> { userById -> | ||
* val userId: Long = userById.id | ||
* } | ||
* post<Users.Add> { addUser -> | ||
* val userName: String = addUser.name | ||
* } | ||
* } | ||
* | ||
* // client-side | ||
* val newUserId = client.post(Users.Add("new_user")) | ||
* val addedUser = client.get(Users.ById(newUserId)) | ||
* ``` | ||
* | ||
* Server: [Type-safe routing](https://ktor.io/docs/type-safe-routing.html) | ||
* | ||
* Client: [Type-safe requests](https://ktor.io/docs/type-safe-request.html) | ||
* | ||
* @see Resource | ||
*/ | ||
public object VoyagerResources : | ||
BaseApplicationPlugin<Application, ResourcesCore.Configuration, ResourcesCore> { | ||
|
||
override val key: AttributeKey<ResourcesCore> = AttributeKey("VoyagerResources") | ||
|
||
override fun install( | ||
pipeline: Application, | ||
configure: ResourcesCore.Configuration.() -> Unit | ||
): ResourcesCore { | ||
val configuration = ResourcesCore.Configuration().apply(configure) | ||
return ResourcesCore(configuration) | ||
} | ||
} | ||
|
||
/** | ||
* Constructs a URL for [resource]. | ||
* | ||
* The class of the [resource] instance **must** be annotated with [Resource]. | ||
*/ | ||
public inline fun <reified T : Any> Application.href(resource: T): String { | ||
return href(plugin(VoyagerResources).resourcesFormat, resource) | ||
} | ||
|
||
/** | ||
* Constructs a URL for [resource]. | ||
* | ||
* The class of the [resource] instance **must** be annotated with [Resource]. | ||
*/ | ||
public inline fun <reified T : Any> Application.href(resource: T, urlBuilder: URLBuilder) { | ||
href(plugin(VoyagerResources).resourcesFormat, resource, urlBuilder) | ||
} |
146 changes: 146 additions & 0 deletions
146
voyager/common/src/dev/programadorthi/routing/voyager/VoyagerResourcesBuilder.kt
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
package dev.programadorthi.routing.voyager | ||
|
||
import cafe.adriel.voyager.core.screen.Screen | ||
import dev.programadorthi.routing.core.OptionalParameterRouteSelector | ||
import dev.programadorthi.routing.core.ParameterRouteSelector | ||
import dev.programadorthi.routing.core.Route | ||
import dev.programadorthi.routing.core.RouteMethod | ||
import dev.programadorthi.routing.core.Routing | ||
import dev.programadorthi.routing.core.application | ||
import dev.programadorthi.routing.core.application.ApplicationCall | ||
import dev.programadorthi.routing.core.application.ApplicationCallPipeline | ||
import dev.programadorthi.routing.core.application.application | ||
import dev.programadorthi.routing.core.application.call | ||
import dev.programadorthi.routing.core.application.plugin | ||
import dev.programadorthi.routing.core.errors.BadRequestException | ||
import dev.programadorthi.routing.core.method | ||
import dev.programadorthi.routing.core.route | ||
import io.ktor.util.AttributeKey | ||
import io.ktor.util.pipeline.PipelineContext | ||
import kotlinx.serialization.KSerializer | ||
import kotlinx.serialization.serializer | ||
|
||
/** | ||
* Registers a typed handler [body] for a resource defined by the [T] class. | ||
* | ||
* A class [T] **must** be annotated with [io.ktor.resources.Resource]. | ||
* | ||
* @param body receives an instance of the typed resource [T] as the first parameter. | ||
*/ | ||
public inline fun <reified T : Any> Route.screen( | ||
noinline body: suspend PipelineContext<Unit, ApplicationCall>.(T) -> Screen | ||
): Route { | ||
val serializer = serializer<T>() | ||
return screen(serializer) { | ||
handle(serializer, body) | ||
} | ||
} | ||
|
||
/** | ||
* Registers a typed handler for a [Screen] defined by the [T] class. | ||
* | ||
* A class [T] **must** be annotated with [io.ktor.resources.Resource]. | ||
*/ | ||
public inline fun <reified T : Screen> Route.screen(): Route = screen<T> { screen -> screen } | ||
|
||
/** | ||
* Registers a typed handler [body] for a [VoyagerRouteMethod] resource defined by the [T] class. | ||
* | ||
* A class [T] **must** be annotated with [io.ktor.resources.Resource]. | ||
* | ||
* @param body receives an instance of the typed resource [T] as the first parameter. | ||
*/ | ||
public inline fun <reified T : Any> Route.screen( | ||
method: RouteMethod, | ||
noinline body: suspend PipelineContext<Unit, ApplicationCall>.(T) -> Screen | ||
): Route { | ||
val serializer = serializer<T>() | ||
lateinit var builtRoute: Route | ||
screen(serializer) { | ||
builtRoute = method(method) { | ||
handle(serializer, body) | ||
} | ||
} | ||
return builtRoute | ||
} | ||
|
||
/** | ||
* Registers a typed handler for a [VoyagerRouteMethod] [Screen] defined by the [T] class. | ||
* | ||
* A class [T] **must** be annotated with [io.ktor.resources.Resource]. | ||
*/ | ||
public inline fun <reified T : Screen> Route.screen( | ||
method: RouteMethod, | ||
): Route = screen<T>(method) { screen -> screen } | ||
|
||
@PublishedApi | ||
internal val ResourceInstanceKey: AttributeKey<Any> = AttributeKey("ResourceInstance") | ||
|
||
/** | ||
* Registers a route [body] for a resource defined by the [T] class. | ||
* | ||
* @param serializer is used to decode the parameters of the request to an instance of the typed resource [T]. | ||
* | ||
* A class [T] **must** be annotated with [io.ktor.resources.Resource]. | ||
*/ | ||
public fun <T : Any> Route.screen( | ||
serializer: KSerializer<T>, | ||
body: Route.() -> Unit | ||
): Route { | ||
val resources = application.plugin(VoyagerResources) | ||
val path = resources.resourcesFormat.encodeToPathPattern(serializer) | ||
val queryParameters = resources.resourcesFormat.encodeToQueryParameters(serializer) | ||
var route = this | ||
// Required for register to parents | ||
route(path = path, name = null) { | ||
route = queryParameters.fold(this) { entry, query -> | ||
val selector = if (query.isOptional) { | ||
OptionalParameterRouteSelector(query.name) | ||
} else { | ||
ParameterRouteSelector(query.name) | ||
} | ||
entry.createChild(selector) | ||
}.apply(body) | ||
} | ||
return route | ||
} | ||
|
||
/** | ||
* Registers a handler [body] for a resource defined by the [T] class. | ||
* | ||
* @param serializer is used to decode the parameters of the request to an instance of the typed resource [T]. | ||
* @param body receives an instance of the typed resource [T] as the first parameter. | ||
*/ | ||
@PublishedApi | ||
internal fun <T : Any> Route.handle( | ||
serializer: KSerializer<T>, | ||
body: suspend PipelineContext<Unit, ApplicationCall>.(T) -> Screen | ||
) { | ||
intercept(ApplicationCallPipeline.Plugins) { | ||
val resources = application.plugin(VoyagerResources) | ||
try { | ||
val resource = | ||
resources.resourcesFormat.decodeFromParameters(serializer, call.parameters) | ||
call.attributes.put(ResourceInstanceKey, resource) | ||
} catch (cause: Throwable) { | ||
throw BadRequestException("Can't transform call to resource", cause) | ||
} | ||
} | ||
|
||
handle { | ||
@Suppress("UNCHECKED_CAST") | ||
val resource = call.attributes[ResourceInstanceKey] as T | ||
screen { | ||
when (resource) { | ||
is Screen -> resource | ||
else -> body(resource) | ||
} | ||
} | ||
} | ||
} | ||
|
||
public inline fun <reified T : Any> Routing.unregisterScreen() { | ||
val serializer = serializer<T>() | ||
val route = screen(serializer) {} | ||
unregisterRoute(route) | ||
} |
26 changes: 26 additions & 0 deletions
26
voyager/common/src/dev/programadorthi/routing/voyager/VoyagerResourcesRoutingExt.kt
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package dev.programadorthi.routing.voyager | ||
|
||
import dev.programadorthi.routing.core.Routing | ||
import dev.programadorthi.routing.core.application | ||
import dev.programadorthi.routing.core.application.pluginOrNull | ||
|
||
public inline fun <reified T : Any> Routing.push(resource: T) { | ||
checkNotNull(application.pluginOrNull(VoyagerResources)) { | ||
"VoyagerResources plugin not installed" | ||
} | ||
push(path = application.href(resource)) | ||
} | ||
|
||
public inline fun <reified T : Any> Routing.replace(resource: T) { | ||
checkNotNull(application.pluginOrNull(VoyagerResources)) { | ||
"VoyagerResources plugin not installed" | ||
} | ||
replace(path = application.href(resource)) | ||
} | ||
|
||
public inline fun <reified T : Any> Routing.replaceAll(resource: T) { | ||
checkNotNull(application.pluginOrNull(VoyagerResources)) { | ||
"VoyagerResources plugin not installed" | ||
} | ||
replaceAll(path = application.href(resource)) | ||
} |
This file contains 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
Oops, something went wrong.