diff --git a/spine-ktor/spine-ktor-client/src/commonMain/kotlin/opensavvy.spine.ktor.client/Client.kt b/spine-ktor/spine-ktor-client/src/commonMain/kotlin/opensavvy.spine.ktor.client/Client.kt index 083fba5..a396558 100644 --- a/spine-ktor/spine-ktor-client/src/commonMain/kotlin/opensavvy.spine.ktor.client/Client.kt +++ b/spine-ktor/spine-ktor-client/src/commonMain/kotlin/opensavvy.spine.ktor.client/Client.kt @@ -9,7 +9,6 @@ import opensavvy.spine.Id import opensavvy.spine.Operation import opensavvy.spine.Parameters import opensavvy.spine.ResourceGroup.AbstractResource -import opensavvy.spine.ktor.NetworkResponse import opensavvy.spine.ktor.toHttp import opensavvy.spine.ktor.toSpine import opensavvy.state.Failure @@ -95,8 +94,7 @@ suspend inline fun = result.body() - response.value + result.body() } else { val body = result.body().ifBlank { "${result.status} with no provided body" } val kind = result.status.toSpine() diff --git a/spine-ktor/spine-ktor-server/src/commonMain/kotlin/opensavvy.spine.ktor.server/Server.kt b/spine-ktor/spine-ktor-server/src/commonMain/kotlin/opensavvy.spine.ktor.server/Server.kt index ca24f8a..f0c40fd 100644 --- a/spine-ktor/spine-ktor-server/src/commonMain/kotlin/opensavvy.spine.ktor.server/Server.kt +++ b/spine-ktor/spine-ktor-server/src/commonMain/kotlin/opensavvy.spine.ktor.server/Server.kt @@ -8,7 +8,6 @@ import opensavvy.logger.Logger.Companion.warn import opensavvy.logger.loggerFor import opensavvy.spine.Operation import opensavvy.spine.Parameters -import opensavvy.spine.ktor.NetworkResponse import opensavvy.spine.ktor.toHttp import opensavvy.state.slice.slice import kotlin.collections.component1 @@ -102,12 +101,7 @@ inline fun ( - val routes: List, - val value: T, -) -- 2.51.2 From 7af0d62ba6d3db1b056404ae1c513135d7a3d78b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Sun, 27 Nov 2022 16:36:56 +0100 Subject: [PATCH 2/2] feat(spine-ktor): Use the Link header to advertise the other endpoints --- .../EndpointsAdvertisement.kt | 33 ++++++++++++++ .../opensavvy.spine.ktor.server/Server.kt | 2 + .../kotlin/opensavvy.spine/Resource.kt | 43 ++++++++++--------- .../kotlin/opensavvy.spine/Route.kt | 6 +++ 4 files changed, 63 insertions(+), 21 deletions(-) create mode 100644 spine-ktor/spine-ktor-server/src/commonMain/kotlin/opensavvy.spine.ktor.server/EndpointsAdvertisement.kt diff --git a/spine-ktor/spine-ktor-server/src/commonMain/kotlin/opensavvy.spine.ktor.server/EndpointsAdvertisement.kt b/spine-ktor/spine-ktor-server/src/commonMain/kotlin/opensavvy.spine.ktor.server/EndpointsAdvertisement.kt new file mode 100644 index 0000000..a0a3913 --- /dev/null +++ b/spine-ktor/spine-ktor-server/src/commonMain/kotlin/opensavvy.spine.ktor.server/EndpointsAdvertisement.kt @@ -0,0 +1,33 @@ +package opensavvy.spine.ktor.server + +import io.ktor.server.application.* +import io.ktor.server.response.* +import opensavvy.spine.Id +import opensavvy.spine.Operation +import opensavvy.spine.Route +import opensavvy.spine.Route.Companion.div +import opensavvy.spine.ktor.toHttp + +fun ApplicationCall.advertiseEndpointsFor(operation: Operation<*, *, *, *, *>, id: Id) { + val resource = operation.resource + + val link = resource.operations + .groupBy( + keySelector = { it.route ?: Route.Root }, + valueTransform = { it.kind } + ) + .map { (route, kinds) -> + val attributes = buildList { + if (route.segments.isEmpty()) + add("canonical") + + for (kind in kinds.toSet()) + add(kind.toHttp().value.lowercase()) + } + "<${Route / id.service / id.resource / route}>; rel=\"${attributes.joinToString(separator = ",")}\"" + } + .joinToString(separator = ", ") + + if (link.isNotBlank()) + response.header("Link", link) +} diff --git a/spine-ktor/spine-ktor-server/src/commonMain/kotlin/opensavvy.spine.ktor.server/Server.kt b/spine-ktor/spine-ktor-server/src/commonMain/kotlin/opensavvy.spine.ktor.server/Server.kt index f0c40fd..30979e7 100644 --- a/spine-ktor/spine-ktor-server/src/commonMain/kotlin/opensavvy.spine.ktor.server/Server.kt +++ b/spine-ktor/spine-ktor-server/src/commonMain/kotlin/opensavvy.spine.ktor.server/Server.kt @@ -90,6 +90,8 @@ inline fun call.receive() } + call.advertiseEndpointsFor(operation, id) + slice { operation.validate(id, body, params, context).bind() diff --git a/spine/src/commonMain/kotlin/opensavvy.spine/Resource.kt b/spine/src/commonMain/kotlin/opensavvy.spine/Resource.kt index aebe77b..70b92fe 100644 --- a/spine/src/commonMain/kotlin/opensavvy.spine/Resource.kt +++ b/spine/src/commonMain/kotlin/opensavvy.spine/Resource.kt @@ -75,6 +75,9 @@ sealed class ResourceGroup { */ abstract val parent: ResourceGroup + protected val _operations = ArrayList>() + val operations: List> get() = _operations + /** * Validates that [id] identifies this resource. */ @@ -128,37 +131,35 @@ sealed class ResourceGroup { protected fun create( route: Route? = null, validate: OperationValidator = { }, - ): Operation, Params, Context> = Operation(this, Operation.Kind.Create, route, validate) + ) = Operation, Params, Context>(this, Operation.Kind.Create, route, validate) + .apply { _operations += this } protected fun edit( route: Route? = null, validate: OperationValidator = { }, - ): Operation = - Operation(this, Operation.Kind.Edit, route) { - validateCorrectId(id) - validateId(id, context) - Operation.ValidatorScope(this, id, body, parameters, context).validate() - } + ) = Operation(this, Operation.Kind.Edit, route) { + validateCorrectId(id) + validateId(id, context) + Operation.ValidatorScope(this, id, body, parameters, context).validate() + }.apply { _operations += this } protected fun action( route: Route, validate: OperationValidator = { }, - ): Operation = - Operation(this, Operation.Kind.Action, route) { - validateCorrectId(id) - validateId(id, context) - Operation.ValidatorScope(this, id, body, parameters, context).validate() - } + ) = Operation(this, Operation.Kind.Action, route) { + validateCorrectId(id) + validateId(id, context) + Operation.ValidatorScope(this, id, body, parameters, context).validate() + }.apply { _operations += this } protected fun delete( route: Route? = null, validate: OperationValidator = { }, - ): Operation = - Operation(this, Operation.Kind.Delete, route) { - validateCorrectId(id) - validateId(id, context) - Operation.ValidatorScope(this, id, body, parameters, context).validate() - } + ) = Operation(this, Operation.Kind.Delete, route) { + validateCorrectId(id) + validateId(id, context) + Operation.ValidatorScope(this, id, body, parameters, context).validate() + }.apply { _operations += this } /** * Instantiates an [Id] for this resource. @@ -217,7 +218,7 @@ sealed class ResourceGroup { validateCorrectId(id) validateId(id, context) validateGetParams(id, parameters, context) - } + }.apply { _operations += this } final override val routeTemplate get() = "${this@ResourceGroup.routeTemplate}/$route" @@ -257,7 +258,7 @@ sealed class ResourceGroup { val get = Operation(this, Operation.Kind.Read) { validateCorrectId(id) validateId(id, context) - } + }.apply { _operations += this } final override val routeTemplate get() = "${this@ResourceGroup.routeTemplate}/{$name}" diff --git a/spine/src/commonMain/kotlin/opensavvy.spine/Route.kt b/spine/src/commonMain/kotlin/opensavvy.spine/Route.kt index 0d4781e..c27794a 100644 --- a/spine/src/commonMain/kotlin/opensavvy.spine/Route.kt +++ b/spine/src/commonMain/kotlin/opensavvy.spine/Route.kt @@ -106,5 +106,11 @@ data class Route(val segments: List) { @Suppress("RemoveRedundantQualifierName") // could be declared on Companion, but I think it's easier to read this way operator fun Route.Companion.div(id: String) = Route(listOf(Segment(id))) + /** + * Shorthand to create a top-level route named [id] (its parent is the [Root]). + */ + @Suppress("RemoveRedundantQualifierName") // could be declared on Companion, but I think it's easier to read this way + operator fun Route.Companion.div(id: Segment) = Route(listOf(id)) + } }