From 1fba67656bc16fb61f6d7d3997e691f7018d2315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Wed, 15 Apr 2026 23:18:00 +0200 Subject: [PATCH 1/2] docs(api): Documentation improvements --- api/src/commonMain/kotlin/Endpoint.kt | 23 ++++++++++++++- api/src/commonMain/kotlin/Failure.kt | 28 ++++++++++++++++++- api/src/commonMain/kotlin/ResolvedResource.kt | 5 ++-- 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/api/src/commonMain/kotlin/Endpoint.kt b/api/src/commonMain/kotlin/Endpoint.kt index 7e34ee0..de72bc1 100644 --- a/api/src/commonMain/kotlin/Endpoint.kt +++ b/api/src/commonMain/kotlin/Endpoint.kt @@ -99,6 +99,11 @@ import kotlin.reflect.KProperty * Note, however, that this guarantees that your code will stop compiling in future versions of this library. */ sealed interface AnyEndpoint { + /** + * The [Resource] this [Endpoint][AnyEndpoint] is declared as a part of. + * + * For example, `GET /api/v2/users` is an endpoint of the resource `/api/v2/users`. + */ val resource: Resource /** @@ -109,7 +114,21 @@ sealed interface AnyEndpoint { /** * Optional path extension from the [Resource] this endpoint is a part of. * - * To access the real path of this endpoint, see [ResolvedEndpoint.path]. + * For example, the endpoint: + * ```kotlin + * object Users : RootResource("/users") { + * val list by get() + * } + * ``` + * has a [path] of `null`, but the endpoint: + * ```kotlin + * object Users : RootResource("/users") { + * val me by get("/me") + * } + * ``` + * has a [path] of `"/me"`. + * + * To access the full path of this endpoint, resolve it (see [ResolvedResource]) then access [ResolvedEndpoint.path]. */ val path: Path.Segment? @@ -126,6 +145,8 @@ sealed interface AnyEndpoint { /** * Constructor for query parameters. * + * This field contains a function that creates a [Parameters] instance from some [ParameterStorage]. + * * If no query parameters are used by this endpoint, this function returns [Parameters.Empty]. */ val buildParameters: ParameterConstructor diff --git a/api/src/commonMain/kotlin/Failure.kt b/api/src/commonMain/kotlin/Failure.kt index 6f129df..701f421 100644 --- a/api/src/commonMain/kotlin/Failure.kt +++ b/api/src/commonMain/kotlin/Failure.kt @@ -92,9 +92,35 @@ sealed interface FailureSpec { * * This type is used to type-safely represent an endpoint that can failure in multiple different ways. * It is not expected that end-users need to use this type, though you may see it appear in inlay hints. + * + * ### Union types + * + * This type emulates a union of two types. + * It can be expanded into a union of arbitrary arity by using [Or] recursively. + * By convention, [Or] instances can only appear in [A], not in [B]. + * + * By convention, [Or] unions always start with the [Never] type. + * + * Therefore, [a] can contain either [Never] or [Or]. [b] can contain only proper failure types, like [ByCode]. + * The following union follows these rules: + * ```kotlin + * val a = Or(Never, Or(ByCode(502), Or(ByCode(404), ByCode(403)))) + * ``` */ class Or( + /** + * The left-hand side of the union. + * + * By convention, this field can only contain [Never] or [Or] instances. + * If this convention is not respected, the union may not be properly recognized by the library methods. + */ val a: A, + /** + * The right-hand side of the union. + * + * By convention, this field can only contain proper failure specifications, like [ByCode]. + * If this convention is not respected, the union may not be properly recognized by the library methods. + */ val b: B, ) : FailureSpec } @@ -126,7 +152,7 @@ private fun FailureSpec.all(): Sequence = sequence { * companion object : FailureCompanion(HttpStatusCode.NotFound) * } * - * // …An endpoint in a resources… + * // …An endpoint in a resource… * val getMe by get("me") * .result() * .failure(UserNotFound) diff --git a/api/src/commonMain/kotlin/ResolvedResource.kt b/api/src/commonMain/kotlin/ResolvedResource.kt index 4877859..e820552 100644 --- a/api/src/commonMain/kotlin/ResolvedResource.kt +++ b/api/src/commonMain/kotlin/ResolvedResource.kt @@ -4,8 +4,9 @@ package opensavvy.spine.api * A resolved [Resource]. * * The [Resource] class represents the _declaration_ of a resource. - * For example, the `/api/users/{user}` is not a 'real' resource. - * This class, [ResolvedResource], represents 'real' resources: '/api/users/111' and '/api/users/222' are possible + * For example, the `/api/users/{user}` is not a 'real' resource: it is a pattern + * from which multiple resources can be created (each user gets their own resource). + * This class, [ResolvedResource], represents 'real' resources: `/api/users/111` and `/api/users/222` are possible * values of this class. * * To instantiate this class, specify the full path from the root, adding necessary runtime information where necessary. -- 2.51.2 From e47f19f3a7e3f58ffaa8bcdaa63a36dc09b7889f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Wed, 15 Apr 2026 23:47:27 +0200 Subject: [PATCH 2/2] docs(client-arrow): Documentation improvements --- client-arrow/README.md | 2 + .../src/commonMain/kotlin/SpineResponse.kt | 254 ++++++++++++++++++ 2 files changed, 256 insertions(+) diff --git a/client-arrow/README.md b/client-arrow/README.md index 605c3b3..958d9c7 100644 --- a/client-arrow/README.md +++ b/client-arrow/README.md @@ -5,3 +5,5 @@ Call a Ktor API described with type-safety in common code, using Arrow typed err + +This module introduces the [body][opensavvy.spine.client.arrow.body] function to type-safely handle errors using Arrow's [Raise DSL][arrow.core.raise.Raise]. diff --git a/client-arrow/src/commonMain/kotlin/SpineResponse.kt b/client-arrow/src/commonMain/kotlin/SpineResponse.kt index 3187302..3ede3f2 100644 --- a/client-arrow/src/commonMain/kotlin/SpineResponse.kt +++ b/client-arrow/src/commonMain/kotlin/SpineResponse.kt @@ -1,6 +1,7 @@ package opensavvy.spine.client.arrow import arrow.core.raise.Raise +import opensavvy.spine.api.AnyEndpoint import opensavvy.spine.api.FailureSpec import opensavvy.spine.api.FailureSpec.Never import opensavvy.spine.api.FailureSpec.Or @@ -8,15 +9,104 @@ import opensavvy.spine.client.SpineResponse import opensavvy.spine.client.bodyOrThrow import opensavvy.spine.client.handle +/** + * Returns the response body. + * + * This overload is inferred when the [endpoint][AnyEndpoint] was declared without any possible failure types. + * + * To declare a failure type, see [failure][AnyEndpoint.Builder.failure] or the [failures tutorial](https://spine.opensavvy.dev/failures.html). + */ suspend inline fun SpineResponse.body(): Out = bodyOrThrow() +/** + * Returns the response body of an endpoint that declares one or more [failures][AnyEndpoint.Builder.failure]. + * + * If any of the declared failures happen, they are raised into the corresponding [Raise] context parameter. + * + * ### Example + * + * For the following API: + * + * ```kotlin + * object Users : RootResource("users") { + * + * object User : DynamicResource("user", Users) { + * + * val get by get() + * .response() + * .failure(HttpStatusCode.NotFound) + * .failure(HttpStatusCode.Forbidden) + * } + * + * val me by get() + * .response() + * .failure(HttpStatusCode.Unauthorized) + * } + * ``` + * + * We can request the different endpoints as so: + * + * ```kotlin + * val client = HttpClient() + * // …configure your HttpClient… + * + * context(_: Raise) + * suspend fun HttpClient.me(): UserDto = + * request(Users / Users.me).body() + * + * context(_: Raise, _: Raise) + * suspend fun HttpClient.getUser(id: String): UserDto = + * request(Users / User(id) / User.get).body() + * ``` + */ context(raise1: Raise) suspend inline fun SpineResponse>>.body(): Out = handle( handle1 = { raise1.raise(it) }, transform = { bodyOrThrow() }, ) +/** + * Returns the response body of an endpoint that declares one or more [failures][AnyEndpoint.Builder.failure]. + * + * If any of the declared failures happen, they are raised into the corresponding [Raise] context parameter. + * + * ### Example + * + * For the following API: + * + * ```kotlin + * object Users : RootResource("users") { + * + * object User : DynamicResource("user", Users) { + * + * val get by get() + * .response() + * .failure(HttpStatusCode.NotFound) + * .failure(HttpStatusCode.Forbidden) + * } + * + * val me by get() + * .response() + * .failure(HttpStatusCode.Unauthorized) + * } + * ``` + * + * We can request the different endpoints as so: + * + * ```kotlin + * val client = HttpClient() + * // …configure your HttpClient… + * + * context(_: Raise) + * suspend fun HttpClient.me(): UserDto = + * request(Users / Users.me).body() + * + * context(_: Raise, _: Raise) + * suspend fun HttpClient.getUser(id: String): UserDto = + * request(Users / User(id) / User.get).body() + * ``` + */ context(raise1: Raise, raise2: Raise) suspend inline fun SpineResponse>, FailureSpec.ByCode>>.body(): Out = handle( handle1 = { raise1.raise(it) }, @@ -24,6 +114,47 @@ suspend inline fun SpineResponse("user", Users) { + * + * val get by get() + * .response() + * .failure(HttpStatusCode.NotFound) + * .failure(HttpStatusCode.Forbidden) + * } + * + * val me by get() + * .response() + * .failure(HttpStatusCode.Unauthorized) + * } + * ``` + * + * We can request the different endpoints as so: + * + * ```kotlin + * val client = HttpClient() + * // …configure your HttpClient… + * + * context(_: Raise) + * suspend fun HttpClient.me(): UserDto = + * request(Users / Users.me).body() + * + * context(_: Raise, _: Raise) + * suspend fun HttpClient.getUser(id: String): UserDto = + * request(Users / User(id) / User.get).body() + * ``` + */ context(raise1: Raise, raise2: Raise, raise3: Raise) suspend inline fun SpineResponse>, FailureSpec.ByCode>, FailureSpec.ByCode>>.body(): Out = handle( handle1 = { raise1.raise(it) }, @@ -32,6 +163,47 @@ suspend inline fun Spine transform = { bodyOrThrow() }, ) +/** + * Returns the response body of an endpoint that declares one or more [failures][AnyEndpoint.Builder.failure]. + * + * If any of the declared failures happen, they are raised into the corresponding [Raise] context parameter. + * + * ### Example + * + * For the following API: + * + * ```kotlin + * object Users : RootResource("users") { + * + * object User : DynamicResource("user", Users) { + * + * val get by get() + * .response() + * .failure(HttpStatusCode.NotFound) + * .failure(HttpStatusCode.Forbidden) + * } + * + * val me by get() + * .response() + * .failure(HttpStatusCode.Unauthorized) + * } + * ``` + * + * We can request the different endpoints as so: + * + * ```kotlin + * val client = HttpClient() + * // …configure your HttpClient… + * + * context(_: Raise) + * suspend fun HttpClient.me(): UserDto = + * request(Users / Users.me).body() + * + * context(_: Raise, _: Raise) + * suspend fun HttpClient.getUser(id: String): UserDto = + * request(Users / User(id) / User.get).body() + * ``` + */ context(raise1: Raise, raise2: Raise, raise3: Raise, raise4: Raise) suspend inline fun SpineResponse>, FailureSpec.ByCode>, FailureSpec.ByCode>, FailureSpec.ByCode>>.body(): Out = handle( handle1 = { raise1.raise(it) }, @@ -41,6 +213,47 @@ suspend inline fun ("user", Users) { + * + * val get by get() + * .response() + * .failure(HttpStatusCode.NotFound) + * .failure(HttpStatusCode.Forbidden) + * } + * + * val me by get() + * .response() + * .failure(HttpStatusCode.Unauthorized) + * } + * ``` + * + * We can request the different endpoints as so: + * + * ```kotlin + * val client = HttpClient() + * // …configure your HttpClient… + * + * context(_: Raise) + * suspend fun HttpClient.me(): UserDto = + * request(Users / Users.me).body() + * + * context(_: Raise, _: Raise) + * suspend fun HttpClient.getUser(id: String): UserDto = + * request(Users / User(id) / User.get).body() + * ``` + */ context(raise1: Raise, raise2: Raise, raise3: Raise, raise4: Raise, raise5: Raise) suspend inline fun SpineResponse>, FailureSpec.ByCode>, FailureSpec.ByCode>, FailureSpec.ByCode>, FailureSpec.ByCode>>.body(): Out = handle( handle1 = { raise1.raise(it) }, @@ -51,6 +264,47 @@ suspend inline fun ("user", Users) { + * + * val get by get() + * .response() + * .failure(HttpStatusCode.NotFound) + * .failure(HttpStatusCode.Forbidden) + * } + * + * val me by get() + * .response() + * .failure(HttpStatusCode.Unauthorized) + * } + * ``` + * + * We can request the different endpoints as so: + * + * ```kotlin + * val client = HttpClient() + * // …configure your HttpClient… + * + * context(_: Raise) + * suspend fun HttpClient.me(): UserDto = + * request(Users / Users.me).body() + * + * context(_: Raise, _: Raise) + * suspend fun HttpClient.getUser(id: String): UserDto = + * request(Users / User(id) / User.get).body() + * ``` + */ context(raise1: Raise, raise2: Raise, raise3: Raise, raise4: Raise, raise5: Raise, raise6: Raise) suspend inline fun SpineResponse>, FailureSpec.ByCode>, FailureSpec.ByCode>, FailureSpec.ByCode>, FailureSpec.ByCode>, FailureSpec.ByCode>>.body(): Out = handle( handle1 = { raise1.raise(it) }, -- 2.51.2