From 17c3ec5755949950dfb7047671a6cf70b1930042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Fri, 20 Dec 2024 13:27:26 +0100 Subject: [PATCH 1/8] breaking: Remove the Spine project The Spine project has been completely rewritten in its own repository: https://gitlab.com/opensavvy/groundwork/spine --- gradle/libs.versions.toml | 15 - settings.gradle.kts | 5 - spine-ktor/build.gradle.kts | 48 --- spine-ktor/spine-ktor-client/build.gradle.kts | 45 --- .../src/commonMain/kotlin/Client.kt | 174 ---------- spine-ktor/spine-ktor-server/build.gradle.kts | 51 --- .../src/commonMain/kotlin/ContextGenerator.kt | 15 - .../kotlin/EndpointsAdvertisement.kt | 33 -- .../src/commonMain/kotlin/GenerateId.kt | 33 -- .../commonMain/kotlin/ResponseStateBuilder.kt | 39 --- .../src/commonMain/kotlin/Server.kt | 187 ----------- .../src/commonTest/kotlin/FailureTest.kt | 92 ------ .../src/commonTest/kotlin/ServerTest.kt | 139 -------- .../src/commonTest/kotlin/TestApi.kt | 65 ---- spine-ktor/src/commonMain/kotlin/Methods.kt | 13 - .../src/commonMain/kotlin/StatusCodes.kt | 21 -- spine/README.md | 5 - spine/build.gradle.kts | 49 --- spine/src/commonMain/kotlin/Id.kt | 42 --- spine/src/commonMain/kotlin/Operation.kt | 81 ----- spine/src/commonMain/kotlin/Parameters.kt | 127 -------- spine/src/commonMain/kotlin/Resource.kt | 299 ------------------ spine/src/commonMain/kotlin/Route.kt | 115 ------- spine/src/commonMain/kotlin/Service.kt | 58 ---- spine/src/commonMain/kotlin/SpineFailure.kt | 45 --- spine/src/commonTest/kotlin/IdTest.kt | 29 -- spine/src/commonTest/kotlin/ParametersTest.kt | 147 --------- spine/src/commonTest/kotlin/RouteTest.kt | 51 --- spine/src/commonTest/kotlin/ServiceTest.kt | 69 ---- tester/build.gradle.kts | 2 - 30 files changed, 2094 deletions(-) delete mode 100644 spine-ktor/build.gradle.kts delete mode 100644 spine-ktor/spine-ktor-client/build.gradle.kts delete mode 100644 spine-ktor/spine-ktor-client/src/commonMain/kotlin/Client.kt delete mode 100644 spine-ktor/spine-ktor-server/build.gradle.kts delete mode 100644 spine-ktor/spine-ktor-server/src/commonMain/kotlin/ContextGenerator.kt delete mode 100644 spine-ktor/spine-ktor-server/src/commonMain/kotlin/EndpointsAdvertisement.kt delete mode 100644 spine-ktor/spine-ktor-server/src/commonMain/kotlin/GenerateId.kt delete mode 100644 spine-ktor/spine-ktor-server/src/commonMain/kotlin/ResponseStateBuilder.kt delete mode 100644 spine-ktor/spine-ktor-server/src/commonMain/kotlin/Server.kt delete mode 100644 spine-ktor/spine-ktor-server/src/commonTest/kotlin/FailureTest.kt delete mode 100644 spine-ktor/spine-ktor-server/src/commonTest/kotlin/ServerTest.kt delete mode 100644 spine-ktor/spine-ktor-server/src/commonTest/kotlin/TestApi.kt delete mode 100644 spine-ktor/src/commonMain/kotlin/Methods.kt delete mode 100644 spine-ktor/src/commonMain/kotlin/StatusCodes.kt delete mode 100644 spine/README.md delete mode 100644 spine/build.gradle.kts delete mode 100644 spine/src/commonMain/kotlin/Id.kt delete mode 100644 spine/src/commonMain/kotlin/Operation.kt delete mode 100644 spine/src/commonMain/kotlin/Parameters.kt delete mode 100644 spine/src/commonMain/kotlin/Resource.kt delete mode 100644 spine/src/commonMain/kotlin/Route.kt delete mode 100644 spine/src/commonMain/kotlin/Service.kt delete mode 100644 spine/src/commonMain/kotlin/SpineFailure.kt delete mode 100644 spine/src/commonTest/kotlin/IdTest.kt delete mode 100644 spine/src/commonTest/kotlin/ParametersTest.kt delete mode 100644 spine/src/commonTest/kotlin/RouteTest.kt delete mode 100644 spine/src/commonTest/kotlin/ServiceTest.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 450065d..59c2848 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,13 +2,10 @@ [versions] slf4j = "2.0.13" # https://github.com/qos-ch/slf4j/tags -ktor = "2.3.2" # https://github.com/ktorio/ktor/releases arrow = "1.2.4" # https://github.com/arrow-kt/arrow/releases lincheck = "2.34" # https://github.com/JetBrains/lincheck/releases -logbackClassic = "1.5.6" # https://github.com/qos-ch/logback/tags kotlinx-datetime = "0.6.1" # https://github.com/Kotlin/kotlinx-datetime/releases kotlinx-coroutines = "1.9.0" # https://github.com/Kotlin/kotlinx.coroutines/releases -kotlinx-serialization = "1.7.1" # https://github.com/Kotlin/kotlinx.serialization/releases prepared = "1.5.2" # https://gitlab.com/opensavvy/prepared/-/releases kotlin-js = "1.0.0-pre.801" # https://central.sonatype.com/artifact/org.jetbrains.kotlin-wrappers/kotlin-js @@ -18,23 +15,11 @@ kotlin-js = "1.0.0-pre.801" # https://central.sonatype.com/artifact/or kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } -kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" } -kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } kotlinJs = { module = "org.jetbrains.kotlin-wrappers:kotlin-js", version.ref = "kotlin-js" } -ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" } -ktor-server-testHost = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" } -ktor-server-contentNegotiation = { module = "io.ktor:ktor-server-content-negotiation", version.ref = "ktor" } -ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } -ktor-client-contentNegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } -ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } -ktor-http = { module = "io.ktor:ktor-http", version.ref = "ktor" } -ktor-kotlinxJson = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } - arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrow" } slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } -logbackClassic = { module = "ch.qos.logback:logback-classic", version.ref = "logbackClassic" } lincheck = { module = "org.jetbrains.kotlinx:lincheck", version.ref = "lincheck" } prepared = { module = "dev.opensavvy.prepared:runner-kotest", version.ref = "prepared" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 874a253..c54acf6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -70,11 +70,6 @@ include( "weak", - "spine", - "spine-ktor", - "spine-ktor:spine-ktor-server", - "spine-ktor:spine-ktor-client", - "logger", "tester", ) diff --git a/spine-ktor/build.gradle.kts b/spine-ktor/build.gradle.kts deleted file mode 100644 index 9f5c81c..0000000 --- a/spine-ktor/build.gradle.kts +++ /dev/null @@ -1,48 +0,0 @@ -@file:Suppress("UNUSED_VARIABLE") - -plugins { - alias(opensavvyConventions.plugins.base) - alias(opensavvyConventions.plugins.kotlin.library) - alias(opensavvyConventions.plugins.aligned.kotlinx.serialization) -} - -kotlin { - jvm() - js(IR) { - browser() - nodejs() - } - - sourceSets { - val commonMain by getting { - dependencies { - api(projects.spine) - - api(libs.kotlinx.serialization.core) - api(libs.ktor.http) - - implementation(projects.logger) - } - } - - val commonTest by getting { - dependencies { - implementation(projects.tester) - - implementation(libs.kotlinx.coroutines.test) - implementation(libs.kotlinx.serialization.json) - } - } - } -} - -library { - name.set("Spine for Ktor (DEPRECATED)") - description.set("Multiplatform API declaration") - homeUrl.set("https://opensavvy.gitlab.io/groundwork/pedestal/api-docs/spine-ktor/index.html") - - license.set { - name.set("Apache 2.0") - url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") - } -} diff --git a/spine-ktor/spine-ktor-client/build.gradle.kts b/spine-ktor/spine-ktor-client/build.gradle.kts deleted file mode 100644 index 1ce2c08..0000000 --- a/spine-ktor/spine-ktor-client/build.gradle.kts +++ /dev/null @@ -1,45 +0,0 @@ -@file:Suppress("UNUSED_VARIABLE") - -plugins { - alias(opensavvyConventions.plugins.base) - alias(opensavvyConventions.plugins.kotlin.library) - alias(opensavvyConventions.plugins.aligned.kotlinx.serialization) -} - -kotlin { - jvm() - js(IR) { - browser() - nodejs() - } - - sourceSets { - val commonMain by getting { - dependencies { - api(libs.ktor.client.core) - - implementation(projects.spineKtor) - implementation(projects.logger) - } - } - - val commonTest by getting { - dependencies { - implementation(projects.tester) - - implementation(libs.kotlinx.coroutines.test) - } - } - } -} - -library { - name.set("Spine for Ktor client (DEPRECATED)") - description.set("Multiplatform API declaration") - homeUrl.set("https://opensavvy.gitlab.io/groundwork/pedestal/api-docs/spine-ktor/spine-ktor-client/index.html") - - license.set { - name.set("Apache 2.0") - url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") - } -} diff --git a/spine-ktor/spine-ktor-client/src/commonMain/kotlin/Client.kt b/spine-ktor/spine-ktor-client/src/commonMain/kotlin/Client.kt deleted file mode 100644 index 225c8d6..0000000 --- a/spine-ktor/spine-ktor-client/src/commonMain/kotlin/Client.kt +++ /dev/null @@ -1,174 +0,0 @@ -package opensavvy.spine.ktor.client - -import io.ktor.client.* -import io.ktor.client.call.* -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.http.* -import io.ktor.serialization.* -import opensavvy.progress.coroutines.mapProgressTo -import opensavvy.spine.Id -import opensavvy.spine.Operation -import opensavvy.spine.Parameters -import opensavvy.spine.ResourceGroup.AbstractResource -import opensavvy.spine.SpineFailure -import opensavvy.spine.ktor.toHttp -import opensavvy.spine.ktor.toSpine -import opensavvy.state.arrow.out -import kotlin.js.JsName -import kotlin.jvm.JvmName - -/** - * Executes a [HttpClient] request, with the information declared in an [Operation]. - * - * This function converts the information declared in [Spine's Operation][Operation] into an actual HTTP request made - * through Ktor's client implementation (see [Ktor's request][io.ktor.client.request.request]). - * It is written to be as similar to use as Ktor's function. - * - * For example, let's imagine an operation `api.users.get` which returns a list of users and takes no parameters. - * ```kotlin - * val client = HttpClient { /* Ktor configuration */ } - * client.request(api.users.get, api.users.get.idOf(), parameters, context) - * ``` - * - * This function will call the [operation]'s [validation][Operation.validate] code. - * - * @param operation The Spine endpoint that should be called. - * @param id The identifier of the [Resource] object on which the endpoint is called. - * It is usually generated using [AbstractResource.idOf]. - * @param input The body of the request, as declared in [operation]. - * @param parameters The query parameters accepted by the request, as declared in [operation]. - * @param context The context of the request, as declared in [operation]. - * @param contentType The MIME type used to encode [input]. JSON by default. - * @param configuration Additional configuration passed to Ktor's `request` function. - * This configuration is applied after the parameters from this request are applied, it is possible to override data set by this function. - */ -suspend inline fun HttpClient.request( - operation: Operation, - id: Id, - input: In, - parameters: Params, - context: Context, - contentType: ContentType = ContentType.Application.Json, - crossinline onResponse: (HttpResponse) -> Unit = {}, - crossinline configuration: HttpRequestBuilder.() -> Unit = {}, -) = out { - mapProgressTo(0.0..0.1) { - operation.validate(id, input, parameters, context).bind() - } - - val result = mapProgressTo(0.1..0.9) { - request { - method = operation.kind.toHttp() - - url { - // {baseUrl}/{service}/{path-to-resource}/{path-to-method} - - // /{service} - appendPathSegments(id.service.segment) - - // /{path-to-resource} - appendPathSegments(id.resource.segments.map { it.segment }) - - // /{path-to-method} (if present) - operation.route?.let { route -> - appendPathSegments(route.segments.map { it.segment }) - } - } - - for ((name, value) in parameters.data) - parameter(name, value) - - contentType(contentType) - setBody(input) - - configuration() - } - } - - mapProgressTo(0.9..0.95) { - onResponse(result) - } - - mapProgressTo(0.95..1.0) { - if (result.status.isSuccess()) { - result.body() - } else { - val kind = result.status.toSpine() - - val failure = try { - SpineFailure(kind, result.body()) - } catch (e: NoTransformationFoundException) { - SpineFailure(kind, result.body().ifBlank { "${result.status} with no provided body" }) - } catch (e: JsonConvertException) { - SpineFailure(kind, result.body().ifBlank { "${result.status} with no provided body" }) - } - - raise(failure) - } - } -} - -// Yes, this is a copy-paste of the function above. -// For some reason, the compiler does not allow 'Nothing' as a reified type parameter, so I have to create an overload -// without that parameter. And because the entire function has to be inline, it has to be a copy. -// Spine is deprecated anyway, and will be completely rewritten when I have the time. -@JvmName("requestNoFailure") -@JsName("requestNoFailure") -suspend inline fun HttpClient.request( - operation: Operation, - id: Id, - input: In, - parameters: Params, - context: Context, - contentType: ContentType = ContentType.Application.Json, - crossinline onResponse: (HttpResponse) -> Unit = {}, - crossinline configuration: HttpRequestBuilder.() -> Unit = {}, -) = out { - mapProgressTo(0.0..0.1) { - operation.validate(id, input, parameters, context).bind() - } - - val result = mapProgressTo(0.1..0.9) { - request { - method = operation.kind.toHttp() - - url { - // {baseUrl}/{service}/{path-to-resource}/{path-to-method} - - // /{service} - appendPathSegments(id.service.segment) - - // /{path-to-resource} - appendPathSegments(id.resource.segments.map { it.segment }) - - // /{path-to-method} (if present) - operation.route?.let { route -> - appendPathSegments(route.segments.map { it.segment }) - } - } - - for ((name, value) in parameters.data) - parameter(name, value) - - contentType(contentType) - setBody(input) - - configuration() - } - } - - mapProgressTo(0.9..0.95) { - onResponse(result) - } - - mapProgressTo(0.95..1.0) { - if (result.status.isSuccess()) { - result.body() - } else { - val kind = result.status.toSpine() - - raise(SpineFailure(kind, result.body().ifBlank { "${result.status} with no provided body" })) - } - } -} diff --git a/spine-ktor/spine-ktor-server/build.gradle.kts b/spine-ktor/spine-ktor-server/build.gradle.kts deleted file mode 100644 index a2593b9..0000000 --- a/spine-ktor/spine-ktor-server/build.gradle.kts +++ /dev/null @@ -1,51 +0,0 @@ -@file:Suppress("UNUSED_VARIABLE") - -plugins { - alias(opensavvyConventions.plugins.base) - alias(opensavvyConventions.plugins.kotlin.library) - alias(opensavvyConventions.plugins.aligned.kotlinx.serialization) -} - -kotlin { - jvm() - - sourceSets { - val commonMain by getting { - dependencies { - api(libs.ktor.server.core) - - implementation(projects.spineKtor) - implementation(projects.logger) - } - } - - val commonTest by getting { - dependencies { - implementation(projects.tester) - - implementation(projects.spineKtor.spineKtorClient) - - implementation(libs.kotlinx.coroutines.test) - implementation(libs.kotlinx.serialization.core) - implementation(libs.kotlinx.serialization.json) - - implementation(libs.ktor.server.testHost) - implementation(libs.ktor.server.contentNegotiation) - implementation(libs.ktor.client.contentNegotiation) - implementation(libs.ktor.client.logging) - implementation(libs.ktor.kotlinxJson) - } - } - } -} - -library { - name.set("Spine for Ktor server (DEPRECATED)") - description.set("Multiplatform API declaration") - homeUrl.set("https://opensavvy.gitlab.io/groundwork/pedestal/api-docs/spine-ktor/spine-ktor-server/index.html") - - license.set { - name.set("Apache 2.0") - url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") - } -} diff --git a/spine-ktor/spine-ktor-server/src/commonMain/kotlin/ContextGenerator.kt b/spine-ktor/spine-ktor-server/src/commonMain/kotlin/ContextGenerator.kt deleted file mode 100644 index d50f3df..0000000 --- a/spine-ktor/spine-ktor-server/src/commonMain/kotlin/ContextGenerator.kt +++ /dev/null @@ -1,15 +0,0 @@ -package opensavvy.spine.ktor.server - -import io.ktor.server.application.* - -/** - * Generates a request's context. - * - * Unlike the client implementation which accepts a context instance directly, the Ktor server must generate the context - * on each request (because the context is tied to the user making the request). - * - * This interface is responsible for generating the context from an [ApplicationCall]. - */ -fun interface ContextGenerator { - suspend fun generate(call: ApplicationCall): Context -} diff --git a/spine-ktor/spine-ktor-server/src/commonMain/kotlin/EndpointsAdvertisement.kt b/spine-ktor/spine-ktor-server/src/commonMain/kotlin/EndpointsAdvertisement.kt deleted file mode 100644 index 2c6c599..0000000 --- a/spine-ktor/spine-ktor-server/src/commonMain/kotlin/EndpointsAdvertisement.kt +++ /dev/null @@ -1,33 +0,0 @@ -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/GenerateId.kt b/spine-ktor/spine-ktor-server/src/commonMain/kotlin/GenerateId.kt deleted file mode 100644 index a0d75af..0000000 --- a/spine-ktor/spine-ktor-server/src/commonMain/kotlin/GenerateId.kt +++ /dev/null @@ -1,33 +0,0 @@ -package opensavvy.spine.ktor.server - -import io.ktor.server.application.* -import opensavvy.spine.Id -import opensavvy.spine.ResourceGroup -import opensavvy.spine.Route - -fun ApplicationCall.generateId(resource: ResourceGroup.AbstractResource): Id { - val values = ArrayList() - - var cursor: ResourceGroup = resource - while (cursor is ResourceGroup.AbstractResource<*, *>) { - values += when (cursor) { - is ResourceGroup.StaticResource<*, *, *, *> -> { - cursor.route.segment - } - - is ResourceGroup.DynamicResource<*, *, *> -> { - val name = cursor.name - val value = parameters[name] - ?: error("Missing path parameter: '{$name}' in '${resource.routeTemplate}'") - value - } - } - cursor = cursor.parent - } - - values.reverse() - - val service = resource.service.name - val route = Route(values.map { Route.Segment(it) }) - return Id(service, route) -} diff --git a/spine-ktor/spine-ktor-server/src/commonMain/kotlin/ResponseStateBuilder.kt b/spine-ktor/spine-ktor-server/src/commonMain/kotlin/ResponseStateBuilder.kt deleted file mode 100644 index b30a0ae..0000000 --- a/spine-ktor/spine-ktor-server/src/commonMain/kotlin/ResponseStateBuilder.kt +++ /dev/null @@ -1,39 +0,0 @@ -package opensavvy.spine.ktor.server - -import arrow.core.raise.Raise -import io.ktor.server.application.* -import opensavvy.spine.Id -import opensavvy.spine.Parameters -import opensavvy.spine.SpineFailure - -/** - * Information available in [route]. - */ -class ResponseStateBuilder( - builder: Raise>, - - /** - * The identifier of the resource being requested. - */ - val id: Id, - - /** - * The body of the request. - */ - val body: In, - - /** - * The query parameters of the request. - */ - val parameters: Params, - - /** - * Ktor's [ApplicationCall] instance, used to set parameters that Spine doesn't recognize (e.g. cookies). - */ - val call: ApplicationCall, - - /** - * The current request's context. - */ - val context: Context, -) : Raise> by builder diff --git a/spine-ktor/spine-ktor-server/src/commonMain/kotlin/Server.kt b/spine-ktor/spine-ktor-server/src/commonMain/kotlin/Server.kt deleted file mode 100644 index 0e2bd7d..0000000 --- a/spine-ktor/spine-ktor-server/src/commonMain/kotlin/Server.kt +++ /dev/null @@ -1,187 +0,0 @@ -package opensavvy.spine.ktor.server - -import arrow.core.raise.either -import io.ktor.server.application.* -import io.ktor.server.request.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import opensavvy.logger.Logger.Companion.warn -import opensavvy.logger.loggerFor -import opensavvy.spine.Operation -import opensavvy.spine.Parameters -import opensavvy.spine.SpineFailure -import opensavvy.spine.ktor.toHttp -import opensavvy.state.arrow.toEither -import kotlin.collections.component1 -import kotlin.collections.component2 -import kotlin.collections.set - -object Server { - val log = loggerFor(this) -} - -/** - * Builds a route to match the specified [operation]. - * - * This function converts the information declared in [Spine's operation][Operation] into a route declaration using - * [Ktor's route][io.ktor.server.routing.route]. - * - * For example, let's imagine an operation `api.users.get` which returns a list of users and takes no parameters. - * ```kotlin - * val context = ContextGenerator { /* see the ContextGenerator interface */ } - * - * routing { - * route(api.users.get, context) { - * // Access the request context and query parameters - * if (!context.admin && parameters.allowArchived) - * // Easily validate values - * markUnauthorized(…) // Automatically converted to HTTP 403 Forbidden - * - * // Set additional Ktor operations not modeled by Spine - * call.setCookie(…) - * - * // Use Pedestal State methods, not call.respond! - * markCompleted(…) - * } - * } - * ``` - * For more information about the data available in the `route` block, see [ResponseStateBuilder]. - * - * This function automatically calls the [operation]'s [validation][Operation.validate] code. - */ -inline fun Route.route( - operation: Operation, - contextGenerator: ContextGenerator, - crossinline block: suspend ResponseStateBuilder.() -> Out, -) { - val path = buildString { - append(operation.resource.routeTemplate) - - for (segment in operation.route?.segments ?: emptyList()) { - append('/') - append(segment.segment) - } - } - - val method = operation.kind.toHttp() - route(path, method) { - handle { - val context = contextGenerator.generate(call) - - val id = call.generateId(operation.resource) - - val params: Params = when { - Params::class == Parameters.Empty::class -> Parameters.Empty as Params - else -> { - val params = Params::class.java - .getConstructor() - .newInstance() - for ((name, values) in call.parameters.entries()) - // if a parameter is added multiple times, only the first one is kept - params.data[name] = values.first() - params - } - } - - val body = when { - // If the expected input is Unit, don't even try to read the body - // Ktor fails to read the body on GET, DELETE and OPTIONS requests. Because we encode them as Unit, - // it's not a problem. - In::class == Unit::class -> Unit as In - // For any other type, delegate to the ContentNegotiation plugin - else -> call.receive() - } - - call.advertiseEndpointsFor(operation, id) - - either, Out> { - operation.validate(id, body, params, context).toEither().bind() - - val responseBuilder = ResponseStateBuilder(this, id, body, params, call, context) - responseBuilder.block() - }.fold( - ifLeft = { - Server.log.warn(it.type) { it.toString() } - when (it) { - is SpineFailure.Message -> call.respond(it.type.toHttp(), it.message ?: "No message") - is SpineFailure.Payload -> call.respond(it.type.toHttp(), it.payload) - } - }, - ifRight = { - call.respond(it) - } - ) - } - } -} - -// Yes, this is a copy-paste of the function above. -// For some reason, the compiler does not allow 'Nothing' as a reified type parameter, so I have to create an overload -// without that parameter. And because the entire function has to be inline, it has to be a copy. -// Spine is deprecated anyway, and will be completely rewritten when I have the time. -@JvmName("routeNoFailure") -inline fun Route.route( - operation: Operation, - contextGenerator: ContextGenerator, - crossinline block: suspend ResponseStateBuilder.() -> Out, -) { - val path = buildString { - append(operation.resource.routeTemplate) - - for (segment in operation.route?.segments ?: emptyList()) { - append('/') - append(segment.segment) - } - } - - val method = operation.kind.toHttp() - route(path, method) { - handle { - val context = contextGenerator.generate(call) - - val id = call.generateId(operation.resource) - - val params: Params = when { - Params::class == Parameters.Empty::class -> Parameters.Empty as Params - else -> { - val params = Params::class.java - .getConstructor() - .newInstance() - for ((name, values) in call.parameters.entries()) - // if a parameter is added multiple times, only the first one is kept - params.data[name] = values.first() - params - } - } - - val body = when { - // If the expected input is Unit, don't even try to read the body - // Ktor fails to read the body on GET, DELETE and OPTIONS requests. Because we encode them as Unit, - // it's not a problem. - In::class == Unit::class -> Unit as In - // For any other type, delegate to the ContentNegotiation plugin - else -> call.receive() - } - - call.advertiseEndpointsFor(operation, id) - - either, Out> { - operation.validate(id, body, params, context).toEither().bind() - - val responseBuilder = ResponseStateBuilder(this, id, body, params, call, context) - responseBuilder.block() - }.fold( - ifLeft = { - Server.log.warn(it.type) { it.toString() } - when (it) { - is SpineFailure.Message -> call.respond(it.type.toHttp(), it.message ?: "No message") - is SpineFailure.Payload -> call.respond(it.type.toHttp(), it.payload) - } - }, - ifRight = { - call.respond(it) - } - ) - } - } -} diff --git a/spine-ktor/spine-ktor-server/src/commonTest/kotlin/FailureTest.kt b/spine-ktor/spine-ktor-server/src/commonTest/kotlin/FailureTest.kt deleted file mode 100644 index e26fe84..0000000 --- a/spine-ktor/spine-ktor-server/src/commonTest/kotlin/FailureTest.kt +++ /dev/null @@ -1,92 +0,0 @@ -package opensavvy.spine.ktor.server - -import io.ktor.client.plugins.logging.* -import io.ktor.serialization.kotlinx.json.* -import io.ktor.server.plugins.callloging.* -import io.ktor.server.testing.* -import kotlinx.serialization.Serializable -import opensavvy.spine.Parameters -import opensavvy.spine.Route -import opensavvy.spine.Route.Companion.div -import opensavvy.spine.Service -import opensavvy.spine.SpineFailure -import opensavvy.spine.ktor.client.request -import opensavvy.state.outcome.failed -import opensavvy.state.outcome.success -import org.junit.Test -import org.slf4j.event.Level -import kotlin.test.assertEquals -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation as ClientContentNegotiation -import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation - -class FailureTest { - - @Serializable - private data class TestFailure(val a: Int) - - private class Api : Service("api") { - inner class Resource : StaticResource("resource") { - - val success = action(Route / "success") - val objectFailure = action(Route / "failWithObject") - val textFailure = action(Route / "failWithText") - - } - - val resource = Resource() - } - - @Test - fun test() = testApplication { - - val api = Api() - - install(ServerContentNegotiation) { - json() - } - - install(CallLogging) { - level = Level.DEBUG - } - - routing { - route(api.resource.success, { }) { - // Nothing to do - } - - route(api.resource.objectFailure, { }) { - raise(SpineFailure(SpineFailure.Type.InvalidRequest, TestFailure(5))) - } - - route(api.resource.textFailure, { }) { - raise(SpineFailure(SpineFailure.Type.InvalidRequest, "5")) - } - } - - val client = createClient { - install(ClientContentNegotiation) { - json() - } - - install(Logging) { - level = LogLevel.ALL - } - } - - assertEquals( - Unit.success(), - client.request(api.resource.success, api.resource.idOf(), Unit, Parameters.Empty, Unit), - ) - - assertEquals( - SpineFailure(SpineFailure.Type.InvalidRequest, TestFailure(5)).failed(), - client.request(api.resource.objectFailure, api.resource.idOf(), Unit, Parameters.Empty, Unit), - ) - - assertEquals( - SpineFailure(SpineFailure.Type.InvalidRequest, "5").failed(), - client.request(api.resource.textFailure, api.resource.idOf(), Unit, Parameters.Empty, Unit), - ) - } - -} diff --git a/spine-ktor/spine-ktor-server/src/commonTest/kotlin/ServerTest.kt b/spine-ktor/spine-ktor-server/src/commonTest/kotlin/ServerTest.kt deleted file mode 100644 index 0f1bcb5..0000000 --- a/spine-ktor/spine-ktor-server/src/commonTest/kotlin/ServerTest.kt +++ /dev/null @@ -1,139 +0,0 @@ -package opensavvy.spine.ktor.server - -import arrow.core.raise.ensure -import io.ktor.client.plugins.logging.* -import io.ktor.serialization.kotlinx.json.* -import io.ktor.server.plugins.callloging.* -import io.ktor.server.testing.* -import opensavvy.logger.Logger.Companion.info -import opensavvy.logger.loggerFor -import opensavvy.spine.* -import opensavvy.spine.Route.Companion.div -import opensavvy.spine.ktor.client.request -import opensavvy.state.outcome.success -import opensavvy.state.outcome.valueOrNull -import org.junit.Test -import org.slf4j.event.Level -import kotlin.test.assertEquals -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation as ClientContentNegotiation -import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation - -class ServerTest { - - private val log = loggerFor(this).apply { - level = opensavvy.logger.LogLevel.TRACE - } - - @Test - fun test() = testApplication { - log.info { "Instantiating the API…" } - val api = TestApi() - - //region Server-side implementation of the API - log.info { "Configuring the server…" } - - install(ServerContentNegotiation) { - json() - } - - install(CallLogging) { - level = Level.DEBUG - } - - log.info { "Declaring the routes…" } - val users = ArrayList() // for the example, we're using a simple list and not a proper database - var nextId = 0 - val context = ContextGenerator {} - - routing { - trace { println(it) } - - route(api.users.get, context) { - val result = users - .filter { parameters.includeArchived || !it.archived } - .map { it.id } - result - } - - route(api.users.create, context) { - val name = body.name - val newId = api.users.id.idOf((nextId++).toString()) - val new = User(newId, name, archived = false) - users += new - Identified(newId, new) - } - - route(api.users.id.get, context) { - val user = users.find { it.id == id } - ensure(user != null) { SpineFailure(SpineFailure.Type.NotFound, "Could not find the user $id") } - user - } - - route(api.users.id.archive, context) { - val userIndex = users.indexOfFirst { it.id == id } - ensure(userIndex >= 0) { SpineFailure(SpineFailure.Type.NotFound, "Could not find user $id") } - val user = users.removeAt(userIndex) - users.add(user.copy(archived = true)) - } - - route(api.users.id.unarchive, context) { - val userIndex = users.indexOfFirst { it.id == id } - ensure(userIndex >= 0) { SpineFailure(SpineFailure.Type.NotFound, "Could not find user $id") } - val user = users.removeAt(userIndex) - users.add(user.copy(archived = false)) - } - - route(api.users.id.delete, context) { - val userIndex = users.indexOfFirst { it.id == id } - ensure(userIndex >= 0) { SpineFailure(SpineFailure.Type.NotFound, "Could not find user $id") } - users.removeAt(userIndex) - } - } - - //endregion - //region Client-side usage of the API - log.info { "Configuring the client…" } - - val client = createClient { - install(ClientContentNegotiation) { - json() - } - - install(Logging) { - level = LogLevel.ALL - } - } - - log.info { "Step 1: query the initial users (empty list)" } - - run { - val params = User.SearchParams().apply { includeArchived = true } - val results = client.request(api.users.get, api.users.get.idOf(), Unit, params, Unit) - - assertEquals(emptyList().success(), results) - } - - log.info { "Step 2: creating two users" } - - run { - val first = - client.request(api.users.create, api.users.create.idOf(), User.New("first"), Parameters.Empty, Unit) - .valueOrNull!! - - val second = - client.request(api.users.create, api.users.create.idOf(), User.New("second"), Parameters.Empty, Unit) - .valueOrNull!! - - assertEquals(User(Id("test", Route / "users" / "0"), "first", archived = false), first.value) - assertEquals(User(Id("test", Route / "users" / "1"), "second", archived = false), second.value) - - val params = User.SearchParams().apply { includeArchived = true } - val results = client.request(api.users.get, api.users.get.idOf(), Unit, params, Unit) - .valueOrNull - - assertEquals(listOf(first.id, second.id), results) - } - - //endregion - } -} diff --git a/spine-ktor/spine-ktor-server/src/commonTest/kotlin/TestApi.kt b/spine-ktor/spine-ktor-server/src/commonTest/kotlin/TestApi.kt deleted file mode 100644 index ffcae33..0000000 --- a/spine-ktor/spine-ktor-server/src/commonTest/kotlin/TestApi.kt +++ /dev/null @@ -1,65 +0,0 @@ -package opensavvy.spine.ktor.server - -import kotlinx.serialization.Serializable -import opensavvy.spine.Id -import opensavvy.spine.Parameters -import opensavvy.spine.Route -import opensavvy.spine.Route.Companion.div -import opensavvy.spine.Service - -//region API objects declaration - -@Serializable -data class User(val id: Id, val name: String, val archived: Boolean) { - - init { - checkUsername(name) - } - - companion object { - private fun checkUsername(name: String) { - for (char in name) - require(!char.isWhitespace()) { "Usernames cannot contain whitespace: '$name'" } - } - } - - @Serializable - data class New(val name: String) { - init { - checkUsername(name) - } - } - - class SearchParams : Parameters() { - var includeArchived: Boolean by parameter("includeArchived") - } - - sealed interface Failures { - data class InvalidUsername(val username: String) : Failures - } -} - -//endregion -//region API endpoints declaration - -class TestApi : Service("test") { - - inner class Users : StaticResource, Nothing, User.SearchParams, Unit>("users") { - inner class Unique : DynamicResource("user") { - - val archive = action(Route / "archive") - - val unarchive = action(Route / "reopen") - - val delete = delete() - } - - val create = create() - - val id = Unique() - } - - val users = Users() -} - -//endregion diff --git a/spine-ktor/src/commonMain/kotlin/Methods.kt b/spine-ktor/src/commonMain/kotlin/Methods.kt deleted file mode 100644 index 1f3905f..0000000 --- a/spine-ktor/src/commonMain/kotlin/Methods.kt +++ /dev/null @@ -1,13 +0,0 @@ -package opensavvy.spine.ktor - -import io.ktor.http.* -import opensavvy.spine.Operation -import opensavvy.spine.Operation.Kind.* - -fun Operation.Kind.toHttp() = when (this) { - Read -> HttpMethod.Get - Create -> HttpMethod.Post - Edit -> HttpMethod.Patch - Delete -> HttpMethod.Delete - Action -> HttpMethod.Put -} diff --git a/spine-ktor/src/commonMain/kotlin/StatusCodes.kt b/spine-ktor/src/commonMain/kotlin/StatusCodes.kt deleted file mode 100644 index 9f716cf..0000000 --- a/spine-ktor/src/commonMain/kotlin/StatusCodes.kt +++ /dev/null @@ -1,21 +0,0 @@ -package opensavvy.spine.ktor - -import io.ktor.http.* -import opensavvy.spine.SpineFailure - -fun HttpStatusCode.toSpine() = when (this) { - HttpStatusCode.NotFound -> SpineFailure.Type.NotFound - HttpStatusCode.Unauthorized -> SpineFailure.Type.Unauthenticated - HttpStatusCode.Forbidden -> SpineFailure.Type.Unauthorized - HttpStatusCode.UnprocessableEntity -> SpineFailure.Type.InvalidRequest - HttpStatusCode.Conflict -> SpineFailure.Type.InvalidState - else -> error("Unexpected error code: $this") -} - -fun SpineFailure.Type.toHttp() = when (this) { - SpineFailure.Type.InvalidRequest -> HttpStatusCode.UnprocessableEntity - SpineFailure.Type.Unauthenticated -> HttpStatusCode.Unauthorized - SpineFailure.Type.Unauthorized -> HttpStatusCode.Forbidden - SpineFailure.Type.NotFound -> HttpStatusCode.NotFound - SpineFailure.Type.InvalidState -> HttpStatusCode.InternalServerError -} diff --git a/spine/README.md b/spine/README.md deleted file mode 100644 index 9d717a6..0000000 --- a/spine/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Module Spine (DEPRECATED) - -> Spine is being completely rewritten [here](https://gitlab.com/opensavvy/spine). This version will soon be deleted. - -Pedestal Spine is a framework allowing the declaration of fullstack Kotlin APIs and data validation in common code, which can later be used to call endpoints on the client-side or implement them on the server-side with type safety. diff --git a/spine/build.gradle.kts b/spine/build.gradle.kts deleted file mode 100644 index 4521fd2..0000000 --- a/spine/build.gradle.kts +++ /dev/null @@ -1,49 +0,0 @@ -@file:Suppress("UNUSED_VARIABLE") - -plugins { - alias(opensavvyConventions.plugins.base) - alias(opensavvyConventions.plugins.kotlin.library) - alias(opensavvyConventions.plugins.aligned.kotlinx.serialization) -} - -kotlin { - jvm() - js(IR) { - browser() - nodejs() - } - - sourceSets { - val commonMain by getting { - dependencies { - api(projects.state) - api(projects.stateArrow) - api(projects.stateCoroutines) - api(libs.kotlinx.serialization.core) - - implementation(projects.logger) - } - } - - val commonTest by getting { - dependencies { - implementation(projects.tester) - implementation(projects.backbone) - - implementation(libs.kotlinx.coroutines.test) - implementation(libs.kotlinx.serialization.json) - } - } - } -} - -library { - name.set("Spine (DEPRECATED)") - description.set("Multiplatform API declaration") - homeUrl.set("https://opensavvy.gitlab.io/groundwork/pedestal/api-docs/spine/index.html") - - license.set { - name.set("Apache 2.0") - url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") - } -} diff --git a/spine/src/commonMain/kotlin/Id.kt b/spine/src/commonMain/kotlin/Id.kt deleted file mode 100644 index 0c3dcc3..0000000 --- a/spine/src/commonMain/kotlin/Id.kt +++ /dev/null @@ -1,42 +0,0 @@ -package opensavvy.spine - -import kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializable -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder - -@Serializable(with = Id.Serializer::class) -data class Id( - val service: Route.Segment, - val resource: Route, -) { - - constructor(service: String, resource: Route) : this(Route.Segment(service), resource) - - override fun toString() = "$service/$resource" - - //region Serializer - internal object Serializer : KSerializer { - override val descriptor = PrimitiveSerialDescriptor("opensavvy.spine.Id", PrimitiveKind.STRING) - - override fun serialize(encoder: Encoder, value: Id) { - encoder.encodeString("${value.service}/${value.resource}") - } - - override fun deserialize(decoder: Decoder): Id { - val segments = decoder.decodeString().split('/') - .map { Route.Segment(it) } - - return Id(segments.first(), Route(segments.drop(1))) - } - } - //endregion -} - -@Serializable -data class Identified( - val id: Id, - val value: T, -) diff --git a/spine/src/commonMain/kotlin/Operation.kt b/spine/src/commonMain/kotlin/Operation.kt deleted file mode 100644 index ac3bd77..0000000 --- a/spine/src/commonMain/kotlin/Operation.kt +++ /dev/null @@ -1,81 +0,0 @@ -package opensavvy.spine - -import arrow.core.raise.Raise -import opensavvy.state.arrow.out -import kotlin.js.JsName - -typealias OperationValidator = suspend Operation.ValidatorScope.() -> Unit - -class Operation( - val resource: ResourceGroup.AbstractResource, - val kind: Kind, - val route: Route? = null, - @JsName("_validate") private val validate: OperationValidator, -) { - - /** - * Instantiates an [Id] for the resource this operation is based on. - * - * This is simply syntax sugar for calling [resource].[idOf][ResourceGroup.AbstractResource.idOf]. - */ - fun idOf(vararg dynamic: String) = resource.idOf(*dynamic) - - suspend fun validate(id: Id, body: In, parameters: Params, context: Context) = out, Unit> { - val scope = ValidatorScope(this, id, body, parameters, context) - scope.validate() - } - - /** - * The various kinds of operations that can be executed on a [ResourceGroup.AbstractResource] instance. - * - * Various operations are differentiated by their semantics: whether they allow caching, whether they are idempotent and what modifications they allow. - */ - enum class Kind { - /** - * Reads some information about a resource. - * - * This operation cannot lead to any change of state on the whole system. - */ - Read, - - /** - * Creates a new resource. - * - * This operation is not idempotent: two [Create] in a row with the same payload will create two different values. - */ - Create, - - /** - * Edits an existing resource. - * - * This operation may or may not be idempotent. - */ - Edit, - - /** - * Executes an arbitrary action on a resource. - * - * This operation should not be confused with [Create], [Edit] and [Delete]. - * - * This operation may or may not be idempotent. - */ - Action, - - /** - * Deletes an existing resource. - */ - Delete, - } - - class ValidatorScope internal constructor( - private val scope: Raise>, - - val id: Id, - - val body: In, - - val parameters: Params, - - val context: Context, - ) : Raise> by scope -} diff --git a/spine/src/commonMain/kotlin/Parameters.kt b/spine/src/commonMain/kotlin/Parameters.kt deleted file mode 100644 index ed370f4..0000000 --- a/spine/src/commonMain/kotlin/Parameters.kt +++ /dev/null @@ -1,127 +0,0 @@ -package opensavvy.spine - -import kotlin.reflect.KProperty - -/** - * Additional parameters for [operations][Operation]. - * - * Resources and operations declare mandatory parameters, such as the identifier of the resource. - * This abstract class allows to declare additional parameters in a type-safe manner. - * - * ### Usage - * - * In common code, declare a class that inherits from [Parameters], and use it to declare the name and type of the available parameters: - * ```kotlin - * class MyRequestParameters : Parameters() { - * var param1: String? by parameter("param1") - * var isSubscribed: Boolean by parameter("is_subscribed", default = false) - * } - * ``` - * - * To create a new parameter bundle, use the standard library [apply] function: - * ```kotlin - * val example = MyRequestParameters().apply { - * param1 = "value" - * isSubscribed = true - * } - * ``` - * - * The values can be accessed in a type-safe manner: - * ```kotlin - * println(example.param1) - * ``` - * The values can also be accessed via their string representation: - * ```kotlin - * println(example.data["param1"]) - * ``` - */ -abstract class Parameters { - - /** - * Internal string representation of the parameters. - */ - val data = HashMap() - - /** - * Declares an optional parameter [name] of type [T]. - * - * If the parameter is missing, reading it will return [default] instead. - */ - fun parameter(name: String, default: T) = Parameter(name, default) - - /** - * Declares a parameter [name] of type [T]. - * - * If [T] is nullable, this function declares an optional parameter with a default value of `null`. - * If [T] is non-nullable, this function declares a mandatory parameter which will throw a [NoSuchElementException] if no value is provided. - */ - fun parameter(name: String) = Parameter(name, defaultValue = null) - - // Internal type used to store the name of a parameter - // End users should not need to use it directly - inner class Parameter<@Suppress("unused") T>(val name: String, val defaultValue: T?) { - - init { - require(name.isNotBlank()) { "The name of a parameter cannot be empty: '$name'" } - require(name.none { it.isWhitespace() }) { "The name of a parameter cannot contain whitespace: '$name'" } - } - } - - /** - * The default parameter instance. - * - * Use this instance for operations that take no parameters. - */ - object Empty : Parameters() - - // Accesses the value and converts it from a String - // This is what the 'by' keyword calls when reading from the value - // Everything it does is explained in the 'parameter' documentation - inline operator fun Parameter.getValue(thisRef: Any?, property: KProperty<*>): T { - val value = data[name] ?: return run { - if (defaultValue is T) - defaultValue - else - throw NoSuchElementException("The parameter '$name' is mandatory, but no value was provided.") - } - - return when (T::class) { - String::class -> value as T - Boolean::class -> value.toBooleanStrict() as T - Byte::class -> value.toByte() as T - Short::class -> value.toShort() as T - Int::class -> value.toInt() as T - Long::class -> value.toLong() as T - UByte::class -> value.toUByte() as T - UShort::class -> value.toUShort() as T - UInt::class -> value.toUInt() as T - ULong::class -> value.toULong() as T - Float::class -> value.toFloat() as T - Double::class -> value.toDouble() as T - else -> throw UnsupportedOperationException("The type ${T::class.simpleName ?: T::class.toString()} is not currently supported in parameters.") - } - } - - // Writes the value and converts it to a String - // This is what the 'by' keyword calls when writing to the value - // Everything it does is explained in the 'parameter' documentation - inline operator fun Parameter.setValue(thisRef: Any?, property: KProperty<*>, value: T) { - val str: String = when (T::class) { - String::class -> value.toString() - Boolean::class -> value.toString() - Byte::class -> value.toString() - Short::class -> value.toString() - Int::class -> value.toString() - Long::class -> value.toString() - UByte::class -> value.toString() - UShort::class -> value.toString() - UInt::class -> value.toString() - ULong::class -> value.toString() - Float::class -> value.toString().removeSuffix(".0") - Double::class -> value.toString().removeSuffix(".0") - else -> throw UnsupportedOperationException("The type ${T::class.simpleName ?: T::class.toString()} is not currently supported in parameters.") - } - - data[name] = str - } -} diff --git a/spine/src/commonMain/kotlin/Resource.kt b/spine/src/commonMain/kotlin/Resource.kt deleted file mode 100644 index c9cd21e..0000000 --- a/spine/src/commonMain/kotlin/Resource.kt +++ /dev/null @@ -1,299 +0,0 @@ -package opensavvy.spine - -import arrow.core.raise.Raise -import arrow.core.raise.ensure -import opensavvy.spine.ResourceGroup.AbstractResource -import opensavvy.spine.Route.Companion.div - -/** - * Common ancestor of [Service] and [AbstractResource]. - * - * This class is used to declare common behavior between services and resources. - * End users should not implement directly. - */ -sealed class ResourceGroup { - - /** - * All [static resources][StaticResource] that appear as direct children of this resource group. - * - * If no static resources were registered, this collection is empty. - */ - val staticRoutes: Map> get() = _staticRoutes - private val _staticRoutes: HashMap> = HashMap() - - /** - * The [dynamic resource][DynamicResource] that appears as a direct child of this resource group. - * - * If no dynamic resource were registered, this property is `null`. - */ - var dynamicRoute: DynamicResource<*, *, *>? = null - private set - - /** - * All resources that appear as direct children of this resource group (sum of [staticRoutes] and [dynamicRoute]). - * - * To access all routes including non-direct children of this resource group, see [routesRecursively]. - */ - val routes: Sequence> - get() = sequence { - yieldAll(staticRoutes.values) - dynamicRoute?.let { yield(it) } - } - - /** - * All resources that appear as children of this resource group, directly or non-directly. - * - * To access only direct children, see [routes]. - */ - val routesRecursively: Sequence> - get() = routes.flatMap { sequenceOf(it) + it.routesRecursively } - - /** - * A pseudo-URI representing this resource. - * - * The value returned by this method is not a valid URI: when dynamic resources are declared, each concrete resource would add its ID in the URI. - */ - abstract val routeTemplate: String - - /** - * Retrieves the [Service] which is responsible for this [ResourceGroup]. - */ - abstract val service: Service - - override fun toString() = routeTemplate - - /** - * Common ancestor of [StaticResource] and [DynamicResource]. - * - * End users should not use this class directly. - */ - sealed class AbstractResource : ResourceGroup() { - - /** - * The direct parent of this resource in the URI hierarchy. - */ - abstract val parent: ResourceGroup - - protected val _operations = ArrayList>() - val operations: List> get() = _operations - - /** - * Validates that [id] identifies this resource. - */ - fun Raise>.validateCorrectId(id: Id) { - ensure( - id.service == service.name - ) { - SpineFailure( - SpineFailure.Type.InvalidState, - "The passed identifier refers to the service '${id.service}', but this resource belongs to the service '${service.name}'" - ) - } - - // Let's check that the resource designated by the ID matches with this resource - var resource: ResourceGroup = this@AbstractResource - var index = id.resource.segments.lastIndex - while (resource is AbstractResource<*, *>) { - val segment = id.resource.segments.getOrNull(index) - ensure( - segment != null - ) { - SpineFailure( - SpineFailure.Type.InvalidState, - "The passed identifier's URI length is too short for this resource: '$id' for resource '${this@AbstractResource}'" - ) - } - - @Suppress("NAME_SHADOWING") // necessary for smart cast because 'resource' is mutable - when (val resource: AbstractResource<*, *> = resource) { - is StaticResource<*, *, *, *> -> { - ensure( - segment == resource.route - ) { - SpineFailure( - SpineFailure.Type.InvalidState, - "The passed identifier's segment #$index doesn't match the resource; expected '${resource.route}' but found '$segment'" - ) - } - } - - is DynamicResource<*, *, *> -> { - // There are no constraints on what IDs look like. - // If we expect an ID, we can't make any verification on the value. - } - - // else -> is impossible - } - - resource = resource.parent - index-- - } - - ensure( - index == -1 - ) { - SpineFailure( - SpineFailure.Type.InvalidState, - "The passed identifier's URI length is too long for this resource: '$id' for resource '${this@AbstractResource}'" - ) - } - } - - /** - * Custom validation based on [id]. - * - * For example, you can override this function to check access rights for read operations. - * By default, this function does nothing. - */ - open suspend fun Raise>.validateId(id: Id, context: Context) {} - - protected fun create( - route: Route? = null, - validate: OperationValidator = { }, - ) = Operation, Params, Context>(this, Operation.Kind.Create, route, validate) - .apply { _operations += this } - - protected fun edit( - route: Route? = null, - validate: OperationValidator = { }, - ) = 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(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(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. - * - * Because this corresponds to instantiating a [Route], it is necessary to replace all dynamic identifiers with - * their concrete values. - * - * The user is responsible for providing the [dynamic] values. - * - * Here are a few examples of proper usage: - * - for resource `/users`: `idOf()` generates the ID `/users` - * - for resource `/users/{user}`: `idOf("52f8")` generates the ID `/users/52f8` - * - for resource `/users/{user}/{pet}`: `idOf("52f8", "a32b")` generates the ID `/users/52f8/a32b` - */ - fun idOf(vararg dynamic: String): Id = idOf(dynamic.asSequence().map { Route.Segment(it) }.iterator()) - - /** - * Instantiates an [Id] for this resource. - * - * This method has a lower level interface, we recommend using the [idOf] overload with a vararg instead. - * - * Implementations of this method should consume the number of dynamic elements they need from the [dynamic] iterator - * and ignore any other value (calling this function with an iterator that has too many elements is correct). - */ - abstract fun idOf(dynamic: Iterator): Id - } - - /** - * A resource that has a hardcoded ID. - * - * For example, top-level resources tend to be static: `/users`. - * Static resources may also appear as children of other resources: `/users/{id}/emails`. - */ - abstract inner class StaticResource protected constructor(route: String) : - AbstractResource() { - - val route = Route.Segment(route) - - init { - require(this@ResourceGroup.staticRoutes[this.route] == null) { "A single resource group cannot have multiple static sub resources with the same route ; ${this@ResourceGroup.staticRoutes[this.route]} has been registered before $this" } - - @Suppress("LeakingThis") - this@ResourceGroup._staticRoutes[this.route] = this - } - - /** - * Validates that [params] allow the user to access this resource. - * - * You should override this function if the parameters impact the access rights. - */ - open suspend fun Raise.validateGetParams(id: Id, params: GetParams, context: Context) {} - - @Suppress("LeakingThis") // Not dangerous because Operation's constructor does nothing - val get = - Operation(this, Operation.Kind.Read) { - validateCorrectId(id) - validateId(id, context) - validateGetParams(id, parameters, context) - }.apply { _operations += this } - - final override val routeTemplate get() = "${this@ResourceGroup.routeTemplate}/$route" - - final override val parent get() = this@ResourceGroup - final override val service get() = this@ResourceGroup.service - - final override fun idOf(dynamic: Iterator): Id { - val parentId = when (val parent = parent) { - is AbstractResource<*, *> -> parent.idOf(dynamic) - is Service -> parent.idOf() - } - - return Id(parentId.service, parentId.resource / route) - } - } - - /** - * A template for resources identified by IDs. - */ - abstract inner class DynamicResource protected constructor( - /** - * The name of the identifier. - * - * When a request is made to this resource, the ID appears as a parameter under this name. - */ - val name: String, - ) : AbstractResource() { - - init { - require(this@ResourceGroup.dynamicRoute == null) { "A single resource group cannot have multiple dynamic sub resources ; ${this@ResourceGroup.dynamicRoute} has been registered before $this" } - - @Suppress("LeakingThis") - this@ResourceGroup.dynamicRoute = this - } - - @Suppress("LeakingThis") // Not dangerous because Operation's constructor does nothing - val get = Operation(this, Operation.Kind.Read) { - validateCorrectId(id) - validateId(id, context) - }.apply { _operations += this } - - final override val routeTemplate get() = "${this@ResourceGroup.routeTemplate}/{$name}" - - final override val parent get() = this@ResourceGroup - final override val service get() = this@ResourceGroup.service - - final override fun idOf(dynamic: Iterator): Id { - val parentId = when (val parent = parent) { - is AbstractResource<*, *> -> parent.idOf(dynamic) - is Service -> parent.idOf() - } - - check(dynamic.hasNext()) { "Not enough dynamic elements were passed to the idOf function of resource '$this'; stuck after '${parentId.resource}'" } - val route = dynamic.next() - - return Id(parentId.service, parentId.resource / route) - } - } -} diff --git a/spine/src/commonMain/kotlin/Route.kt b/spine/src/commonMain/kotlin/Route.kt deleted file mode 100644 index e61f300..0000000 --- a/spine/src/commonMain/kotlin/Route.kt +++ /dev/null @@ -1,115 +0,0 @@ -package opensavvy.spine - -import kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializable -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder - -/** - * An API route. - * - * This is a suffix that can be added to the API URI to create a valid URI to a route of the API. - * To convert this class into a URI-compatible format, use [toString]. - * - * To conveniently create instances of this class, two shorthands are provided: - * ```kotlin - * val first = Route / "test" - * val second = first / "other" - * ``` - */ -@Serializable(with = Route.Serializer::class) -data class Route(val segments: List) { - - /** - * A route segment. - * - * Segments may only be composed of characters explicitly unreserved in URIs. - */ - @Serializable(with = Segment.Serializer::class) - data class Segment(val segment: String) { - init { - for (char in segment) { - if (char == '&' || char == '/' || char == '?') - throw IllegalArgumentException("A route segment cannot be composed of the characters '&', '/', or '?'; found character '$char' in segment '$segment'") - } - } - - override fun toString() = segment - - /** - * Serializer for [Segment]. - * - * Without this serializer, segments would be serialized as objects: `{ "segment": "foo" }`. - * With this serializer, segments are serialized as values: `"foo"`. - */ - internal object Serializer : KSerializer { - override val descriptor = PrimitiveSerialDescriptor("opensavvy.spine.Route.Segment", PrimitiveKind.STRING) - - override fun serialize(encoder: Encoder, value: Segment) { - encoder.encodeString(value.segment) - } - - override fun deserialize(decoder: Decoder): Segment { - return Segment(decoder.decodeString()) - } - } - } - - override fun toString() = segments.joinToString(separator = "/") - - /** - * Serializer for [Route]. - * - * Without this serializer, routes would be serialized as objects of arrays: `{ segments: ["foo", "bar"] }`. - * With this serializer, routes are serialized as values: `"foo/bar"`. - */ - internal object Serializer : KSerializer { - override val descriptor = PrimitiveSerialDescriptor("opensavvy.spine.Route", PrimitiveKind.STRING) - - override fun serialize(encoder: Encoder, value: Route) { - encoder.encodeString(value.toString()) - } - - override fun deserialize(decoder: Decoder): Route { - return Route(decoder.decodeString().split("/").map { Segment(it) }) - } - } - - companion object { - - /** - * The empty [Route]. - */ - val Root = Route(emptyList()) - - /** - * Shorthand to create a sub-route named [id] from the current route. - */ - operator fun Route.div(id: String) = Route(segments + Segment(id)) - - /** - * Shorthand to create a sub-route named [id] from the current route. - */ - operator fun Route.div(id: Segment) = Route(segments + id) - - /** - * Shorthand to concatenate [other] at the end of this route. - */ - operator fun Route.div(other: Route) = Route(segments + other.segments) - - /** - * 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: 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)) - - } -} diff --git a/spine/src/commonMain/kotlin/Service.kt b/spine/src/commonMain/kotlin/Service.kt deleted file mode 100644 index 97311f4..0000000 --- a/spine/src/commonMain/kotlin/Service.kt +++ /dev/null @@ -1,58 +0,0 @@ -package opensavvy.spine - -/** - * Services are the top-level endpoints of a Spine API. - * - * ### Spine API structure - * - * A Spine API is composed of multiple services. - * Services are logical groupings of resources identified by a unique name. - * Services may be heterogeneous: multiple services exposed by a single server may have a different structure. - * - * Exposing multiple services may be necessary to represent: - * - multiple versions of the same API (with services `v1`, `v2`…) - * - multiple implementations of the same API (with services `mongodb`, `external`…) - * - all of the above - * - * ### Declaring a new API - * - * APIs are inner class structures with an outer class that inherits from `Service`: - * ```kotlin - * class Api1 : Service("v1") { - * inner class Users : StaticResource("users") - * - * val users = Users() // this is important! - * } - * ``` - * In the current version, it is necessary to instantiate each inner class once as an attribute. - * - * Resources are declared as [StaticResource][ResourceGroup.StaticResource] or [DynamicResource][ResourceGroup.DynamicResource]. - * Resources can be nested. - */ -abstract class Service( - /** - * The name of this service. - */ - val name: Route.Segment, -) : ResourceGroup() { - - /** - * Creates a [Service]. - * - * Because [name] appears in URIs, it must satisfy all constraints of [Route.Segment]. - */ - constructor(name: String) : this(Route.Segment(name)) - - override val routeTemplate get() = name.segment - - override val service get() = this - - /** - * Instantiates an imaginary [Id] for the whole service. - * - * Services are not resources, they cannot be queried. - * However, it may be convenient to generate an ID for them. - */ - fun idOf() = Id(name, Route.Root) - -} diff --git a/spine/src/commonMain/kotlin/SpineFailure.kt b/spine/src/commonMain/kotlin/SpineFailure.kt deleted file mode 100644 index a86205a..0000000 --- a/spine/src/commonMain/kotlin/SpineFailure.kt +++ /dev/null @@ -1,45 +0,0 @@ -package opensavvy.spine - -sealed class SpineFailure { - - abstract val type: Type - - abstract val payload: Payload? - - data class Message( - override val type: Type, - val message: String? = null, - ) : SpineFailure() { - override val payload: Nothing? - get() = null - - override fun toString() = "$type: “${message}”" - } - - data class Payload( - override val type: Type, - override val payload: Payload, - ) : SpineFailure() { - - override fun toString() = "$type: “${payload}”" - } - - enum class Type { - Unauthenticated, - Unauthorized, - NotFound, - InvalidRequest, - InvalidState, - ; - } -} - -fun SpineFailure( - type: SpineFailure.Type, - message: String? = null, -) = SpineFailure.Message(type, message) - -fun SpineFailure( - type: SpineFailure.Type, - payload: Payload, -) = SpineFailure.Payload(type, payload) diff --git a/spine/src/commonTest/kotlin/IdTest.kt b/spine/src/commonTest/kotlin/IdTest.kt deleted file mode 100644 index 3617b21..0000000 --- a/spine/src/commonTest/kotlin/IdTest.kt +++ /dev/null @@ -1,29 +0,0 @@ -package opensavvy.spine - -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import opensavvy.spine.Route.Companion.div -import kotlin.test.Test -import kotlin.test.assertEquals - -class IdTest { - - @Test - fun test() { - val id = Id(service = "v2", resource = Route / "int" / "35eb") - - assertEquals("v2/int/35eb", id.toString()) - } - - @Test - fun serialize() { - val id = Id(service = "v2", resource = Route / "int" / "35eb") - - val serialized = Json.encodeToString(id) - assertEquals("v2/int/35eb", id.toString()) - - val deserialized = Json.decodeFromString(serialized) - assertEquals(id, deserialized) - } -} diff --git a/spine/src/commonTest/kotlin/ParametersTest.kt b/spine/src/commonTest/kotlin/ParametersTest.kt deleted file mode 100644 index 1337783..0000000 --- a/spine/src/commonTest/kotlin/ParametersTest.kt +++ /dev/null @@ -1,147 +0,0 @@ -package opensavvy.spine - -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFails -import kotlin.test.assertTrue - -class ParametersTest { - - @Test - fun mandatory() { - class MandatoryParams : Parameters() { - var archived: Boolean by parameter("archived") - var private: Boolean by parameter("private") - } - - val params = MandatoryParams().apply { - archived = true - } - - assertTrue(params.archived) - assertFails { params.private } - - assertEquals( - mapOf( - "archived" to "true" - ), params.data - ) - } - - @Test - fun optional() { - class OptionalParams : Parameters() { - var archived: Boolean? by parameter("archived") - var private: Boolean? by parameter("private") - } - - val params = OptionalParams().apply { - archived = true - } - - assertEquals(true, params.archived) - assertEquals(null, params.private) - - assertEquals( - mapOf( - "archived" to "true" - ), params.data - ) - } - - @Test - fun default() { - class DefaultValueParams : Parameters() { - var archived: Boolean by parameter("archived", false) - var private: Boolean by parameter("private", false) - } - - val params = DefaultValueParams().apply { - archived = true - } - - assertEquals(true, params.archived) - assertEquals(false, params.private) - - assertEquals( - mapOf( - "archived" to "true" - ), params.data - ) - } - - @Test - fun types() { - class Types : Parameters() { - var string: String by parameter("string") - var bool: Boolean by parameter("bool") - - var byte: Byte by parameter("byte") - var short: Short by parameter("short") - var int: Int by parameter("int") - var long: Long by parameter("long") - - var ubyte: UByte by parameter("ubyte") - var ushort: UShort by parameter("ushort") - var uint: UInt by parameter("uint") - var ulong: ULong by parameter("ulong") - - var float: Float by parameter("float") - var double: Double by parameter("double") - } - - val params = Types().apply { - string = "thing" - bool = true - - byte = 1 - short = 2 - int = 3 - long = 4 - - ubyte = 5u - ushort = 6u - uint = 7u - ulong = 8u - - float = 9f - double = 10.0 - } - - assertEquals("thing", params.string) - assertEquals(true, params.bool) - - assertEquals(1, params.byte) - assertEquals(2, params.short) - assertEquals(3, params.int) - assertEquals(4, params.long) - - assertEquals(5u, params.ubyte) - assertEquals(6u, params.ushort) - assertEquals(7u, params.uint) - assertEquals(8u, params.ulong) - - assertEquals(9f, params.float) - assertEquals(10.0, params.double) - - assertEquals( - mapOf( - "string" to "thing", - "bool" to "true", - - "byte" to "1", - "short" to "2", - "int" to "3", - "long" to "4", - - "ubyte" to "5", - "ushort" to "6", - "uint" to "7", - "ulong" to "8", - - "float" to "9", - "double" to "10", - ), params.data - ) - } -} diff --git a/spine/src/commonTest/kotlin/RouteTest.kt b/spine/src/commonTest/kotlin/RouteTest.kt deleted file mode 100644 index cd604c8..0000000 --- a/spine/src/commonTest/kotlin/RouteTest.kt +++ /dev/null @@ -1,51 +0,0 @@ -package opensavvy.spine - -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import opensavvy.spine.Route.Companion.div -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFails - -class RouteTest { - - @Test - fun root() { - assertEquals("", Route.Root.toString()) - } - - @Test - fun firstElement() { - assertEquals("test", (Route / "test").toString()) - assertEquals("test35-7_12~.", (Route / "test35-7_12~.").toString()) - - assertFails { Route / "test/with/slashes" } - } - - @Test - fun secondElement() { - assertEquals("first/second", (Route / "first" / "second").toString()) - } - - @Test - fun serializeSegment() { - val segment = Route.Segment("whatever-hello") - - val segmentJson = Json.encodeToString(segment) - assertEquals("\"whatever-hello\"", segmentJson) - - assertEquals(Route.Segment("whatever-hello"), Json.decodeFromString(segmentJson)) - } - - @Test - fun serializeRoute() { - val route = Route / "whatever" / "hello" - - val routeJson = Json.encodeToString(route) - assertEquals("\"whatever/hello\"", routeJson) - - assertEquals(Route / "whatever" / "hello", Json.decodeFromString(routeJson)) - } - -} diff --git a/spine/src/commonTest/kotlin/ServiceTest.kt b/spine/src/commonTest/kotlin/ServiceTest.kt deleted file mode 100644 index a5c5cdc..0000000 --- a/spine/src/commonTest/kotlin/ServiceTest.kt +++ /dev/null @@ -1,69 +0,0 @@ -@file:OptIn(ExperimentalCoroutinesApi::class) -@file:Suppress("unused") - -package opensavvy.spine - -import kotlinx.coroutines.ExperimentalCoroutinesApi -import opensavvy.backbone.Ref -import opensavvy.spine.Route.Companion.div - -/* - * Example API: - * /departments List of IDs of departments - * /departments?public=true List of IDs of public departments - * /departments/{id} Info about a single department - * /departments/{id}/users Query the list of users in that department - * /users List of IDs of users - * /users/{id} Info about a single user - * /users/{id}/departments List of departments a user is a part of - * /users/{id}/join PATCH - * /users/{id}/leave PATCH - */ - -private data class Department(val name: String) { - class SearchParams : Parameters() { - var showArchived by parameter("archived", default = false) - } -} - -private data class User(val name: String, val admin: Boolean) { - - data class New(val name: String) - - data class Rename(val name: String) -} - -private class Context(val user: Ref, User>) - -private class Api : Service("v2") { - inner class Departments : StaticResource, Nothing, Department.SearchParams, Context>("departments") { - inner class Unique : DynamicResource("department") { - inner class Users : StaticResource, Nothing, Parameters.Empty, Context>("users") - - val users = Users() - } - - val id = Unique() - } - - inner class Users : StaticResource, Nothing, Parameters.Empty, Context>("users") { - inner class Unique : DynamicResource("user") { - inner class Departments : StaticResource, Nothing, Parameters.Empty, Context>("departments") - - val join = action(Route / "join") - - val leave = action(Route / "leave") - - val rename = edit(Route / "name") - - val departments = Departments() - } - - val create = create() - - val id = Unique() - } - - val departments = Departments() - val users = Users() -} diff --git a/tester/build.gradle.kts b/tester/build.gradle.kts index a7cce85..377c2e3 100644 --- a/tester/build.gradle.kts +++ b/tester/build.gradle.kts @@ -19,8 +19,6 @@ kotlin { dependencies { api(kotlin("test-common")) api(kotlin("test-annotations-common")) - - implementation(libs.logbackClassic) } } -- 2.51.2 From d073ec64ed1af5773f199b69df23a4a49641e890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Fri, 20 Dec 2024 13:36:43 +0100 Subject: [PATCH 2/8] test(backbone): Rewrite tests using Prepared --- backbone/build.gradle.kts | 4 +- .../commonTest/kotlin/BackboneCacheTest.kt | 67 ++++++------------- 2 files changed, 22 insertions(+), 49 deletions(-) diff --git a/backbone/build.gradle.kts b/backbone/build.gradle.kts index 2fd4dc3..5b4d460 100644 --- a/backbone/build.gradle.kts +++ b/backbone/build.gradle.kts @@ -29,10 +29,8 @@ kotlin { val commonTest by getting { dependencies { - implementation(projects.tester) + implementation(libs.bundles.prepared) implementation(projects.stateArrow) - - api(libs.kotlinx.coroutines.test) } } } diff --git a/backbone/src/commonTest/kotlin/BackboneCacheTest.kt b/backbone/src/commonTest/kotlin/BackboneCacheTest.kt index dd138ad..c29beab 100644 --- a/backbone/src/commonTest/kotlin/BackboneCacheTest.kt +++ b/backbone/src/commonTest/kotlin/BackboneCacheTest.kt @@ -1,72 +1,47 @@ package opensavvy.backbone import arrow.core.raise.ensure -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.Job -import kotlinx.coroutines.test.runTest import opensavvy.cache.cache +import opensavvy.prepared.runner.kotest.PreparedSpec import opensavvy.state.arrow.out import opensavvy.state.outcome.valueOrNull -import kotlin.test.Test -import kotlin.test.assertEquals -@OptIn(ExperimentalCoroutinesApi::class) -class BackboneCacheTest { +private data class BasicRef(val id: String, val backbone: Bone) : Ref { - data class BasicRef(val id: String, val backbone: Bone) : Ref { - - override fun request() = backbone.cache[this] - } + override fun request() = backbone.cache[this] +} - // Id("12") -> 12 - class Bone : Backbone { - val cache = cache { - out { - val int = it.id.toIntOrNull() - ensure(int != null) { Invalid } - int - } +// Id("12") -> 12 +private class Bone : Backbone { + val cache = cache { + out { + val int = it.id.toIntOrNull() + ensure(int != null) { Invalid } + int } - - fun of(int: Int) = BasicRef(int.toString(), this) - - object Invalid } - @Test - fun default() = runTest { - val bone = Bone() - val id5 = bone.of(5) - val id2 = bone.of(2) - - assertEquals(5, id5.now().valueOrNull) - assertEquals(2, id2.now().valueOrNull) + fun of(int: Int) = BasicRef(int.toString(), this) - bone.cache.expire(id2) - assertEquals(2, id2.now().valueOrNull) - } + object Invalid +} - @Test - fun batching() = runTest { - val job = Job() +class BackboneCacheTest : PreparedSpec({ + test("Default") { val bone = Bone() val id5 = bone.of(5) val id2 = bone.of(2) - assertEquals(5, id5.now().valueOrNull) - assertEquals(2, id2.now().valueOrNull) + check(id5.now().valueOrNull == 5) + check(id2.now().valueOrNull == 2) bone.cache.expire(id2) - assertEquals(2, id2.now().valueOrNull) - - job.cancel() + check(id2.now().valueOrNull == 2) } - @Test - fun companions() { + test("Existence of companions") { println(Ref) println(Backbone) - println() } -} +}) -- 2.51.2 From 124da764310115d2e99e8629d90f7f4d3dcfc2f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Fri, 20 Dec 2024 15:24:50 +0100 Subject: [PATCH 3/8] breaking: Remove the Tester project, switch away from kotlin-test --- cache-blocking/build.gradle.kts | 4 +- cache/build.gradle.kts | 2 - .../commonTest/kotlin/ContextualCacheTest.kt | 102 +++++++++--------- logger/build.gradle.kts | 2 +- logger/src/commonTest/kotlin/LoggerTest.kt | 14 ++- progress-coroutines/build.gradle.kts | 2 - .../kotlin/CoroutineProgressReporterTest.kt | 22 ++-- .../kotlin/ProgressReportTransformerTest.kt | 24 ++--- .../kotlin/StateFlowProgressReporterTest.kt | 18 ++-- progress/build.gradle.kts | 1 - .../kotlin/SimpleLoadingImplementationTest.kt | 11 +- .../kotlin/report/MapProgressReporterTest.kt | 3 +- settings.gradle.kts | 3 +- state-arrow/build.gradle.kts | 1 - state-coroutines/build.gradle.kts | 3 - .../src/commonTest/kotlin/AccumulatorsTest.kt | 3 +- .../commonTest/kotlin/CaptureProgressTest.kt | 5 +- .../kotlin/ProgressiveFlowAccessorsTest.kt | 11 +- state/build.gradle.kts | 3 - .../src/commonTest/kotlin/ProgressionTest.kt | 25 +++-- .../commonTest/kotlin/arrow/ConverterTest.kt | 46 ++------ .../kotlin/progressive/AccessorsTest.kt | 26 ++--- .../kotlin/progressive/AccumulatorsTest.kt | 5 +- .../kotlin/progressive/BuilderTest.kt | 41 ++----- .../kotlin/progressive/ConditionalTest.kt | 26 +++-- .../kotlin/progressive/MappingTest.kt | 31 ++---- tester/README.md | 3 - tester/build.gradle.kts | 37 ------- tester/src/commonMain/kotlin/Marker.kt | 6 -- tester/src/jvmMain/resources/logback.xml | 12 --- 30 files changed, 161 insertions(+), 331 deletions(-) delete mode 100644 tester/README.md delete mode 100644 tester/build.gradle.kts delete mode 100644 tester/src/commonMain/kotlin/Marker.kt delete mode 100644 tester/src/jvmMain/resources/logback.xml diff --git a/cache-blocking/build.gradle.kts b/cache-blocking/build.gradle.kts index 40e7b6d..36d52eb 100644 --- a/cache-blocking/build.gradle.kts +++ b/cache-blocking/build.gradle.kts @@ -19,9 +19,9 @@ kotlin { val commonTest by getting { dependencies { - implementation(projects.tester) - implementation(libs.kotlinx.coroutines.test) + implementation(libs.bundles.prepared) implementation(libs.lincheck) + implementation(opensavvyConventions.aligned.kotlin.test) // Needed by Lincheck implementation(projects.stateArrow) } } diff --git a/cache/build.gradle.kts b/cache/build.gradle.kts index 894d97c..04e5c2d 100644 --- a/cache/build.gradle.kts +++ b/cache/build.gradle.kts @@ -23,8 +23,6 @@ kotlin { sourceSets.commonTest.dependencies { implementation(libs.bundles.prepared) - implementation(projects.tester) - implementation(libs.kotlinx.coroutines.test) implementation(projects.stateArrow) } } diff --git a/cache/src/commonTest/kotlin/ContextualCacheTest.kt b/cache/src/commonTest/kotlin/ContextualCacheTest.kt index 4f0c28f..c90b5f8 100644 --- a/cache/src/commonTest/kotlin/ContextualCacheTest.kt +++ b/cache/src/commonTest/kotlin/ContextualCacheTest.kt @@ -1,62 +1,64 @@ package opensavvy.cache +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.map import kotlinx.coroutines.job -import kotlinx.coroutines.test.runTest import opensavvy.cache.contextual.batchingCache import opensavvy.cache.contextual.cache import opensavvy.cache.contextual.cachedInMemory import opensavvy.cache.contextual.expireAfter +import opensavvy.prepared.compat.kotlinx.datetime.clock +import opensavvy.prepared.runner.kotest.PreparedSpec +import opensavvy.prepared.suite.backgroundScope +import opensavvy.prepared.suite.time import opensavvy.state.arrow.out import opensavvy.state.coroutines.now import opensavvy.state.outcome.successful -import kotlin.test.Test -import kotlin.test.assertEquals import kotlin.time.Duration.Companion.minutes - -class ContextualCacheTest { - - private val data = generateSequence(0) { it + 1 } - - private data class Identifier( - val even: Boolean, - val odd: Boolean, - ) - - private data class Context( - val startAt: Int, - val limit: Int, - ) - - private fun createCache() = cache> { id, context -> - out { - data - .drop(context.startAt) - .filter { id.even || it % 2 != 0 } - .filter { id.odd || it % 2 != 1 } - .take(context.limit) - .toList() - } +import kotlin.time.ExperimentalTime + +private val data = generateSequence(0) { it + 1 } + +private data class Identifier( + val even: Boolean, + val odd: Boolean, +) + +private data class Context( + val startAt: Int, + val limit: Int, +) + +private fun createCache() = cache> { id, context -> + out { + data + .drop(context.startAt) + .filter { id.even || it % 2 != 0 } + .filter { id.odd || it % 2 != 1 } + .take(context.limit) + .toList() } +} + +@OptIn(ExperimentalTime::class, ExperimentalCoroutinesApi::class) +class ContextualCacheTest : PreparedSpec({ - @Test - fun read() = runTest { + test("Read") { val cache = createCache() .cachedInMemory(backgroundScope.coroutineContext.job) - .expireAfter(2.minutes, backgroundScope, testClock) + .expireAfter(2.minutes, backgroundScope, time.clock) val expected = (0 until 100).toList().successful() val actual = cache[Identifier(even = true, odd = true), Context(0, 100)].now() - assertEquals(expected, actual) + check(actual == expected) } - @Test - fun expire() = runTest { + test("Expire") { val cache = createCache() .cachedInMemory(backgroundScope.coroutineContext.job) - .expireAfter(2.minutes, backgroundScope, testClock) + .expireAfter(2.minutes, backgroundScope, time.clock) // Update with invalid values, so we can notice whether they are expired+queried or not cache.update(Identifier(even = true, odd = true), Context(0, 100), (0 until 10).toList()) @@ -66,19 +68,19 @@ class ContextualCacheTest { run { val expected = (0 until 10).toList().successful() val actual = cache[Identifier(even = true, odd = true), Context(0, 100)].now() - assertEquals(expected, actual) + check(expected == actual) } run { val expected = (0 until 10).toList().successful() val actual = cache[Identifier(even = true, odd = true), Context(10, 100)].now() - assertEquals(expected, actual) + check(expected == actual) } run { val expected = (0 until 10).toList().successful() val actual = cache[Identifier(even = true, odd = false), Context(0, 100)].now() - assertEquals(expected, actual) + check(expected == actual) } // The first value should be updated, but the second one should be unchanged @@ -87,19 +89,19 @@ class ContextualCacheTest { run { val expected = (0 until 100).toList().successful() val actual = cache[Identifier(even = true, odd = true), Context(0, 100)].now() - assertEquals(expected, actual) + check(expected == actual) } run { val expected = (0 until 10).toList().successful() val actual = cache[Identifier(even = true, odd = true), Context(10, 100)].now() - assertEquals(expected, actual) + check(expected == actual) } run { val expected = (0 until 10).toList().successful() val actual = cache[Identifier(even = true, odd = false), Context(0, 100)].now() - assertEquals(expected, actual) + check(expected == actual) } // The first and second values should be updated @@ -108,19 +110,19 @@ class ContextualCacheTest { run { val expected = (0 until 100).toList().successful() val actual = cache[Identifier(even = true, odd = true), Context(0, 100)].now() - assertEquals(expected, actual) + check(expected == actual) } run { val expected = (10 until 110).toList().successful() val actual = cache[Identifier(even = true, odd = true), Context(10, 100)].now() - assertEquals(expected, actual) + check(expected == actual) } run { val expected = (0 until 10).toList().successful() val actual = cache[Identifier(even = true, odd = false), Context(0, 100)].now() - assertEquals(expected, actual) + check(expected == actual) } // The last value should be updated @@ -129,24 +131,23 @@ class ContextualCacheTest { run { val expected = (0 until 100).toList().successful() val actual = cache[Identifier(even = true, odd = true), Context(0, 100)].now() - assertEquals(expected, actual) + check(expected == actual) } run { val expected = (10 until 110).toList().successful() val actual = cache[Identifier(even = true, odd = true), Context(10, 100)].now() - assertEquals(expected, actual) + check(expected == actual) } run { val expected = (0 until 200 step 2).toList().successful() val actual = cache[Identifier(even = true, odd = false), Context(0, 100)].now() - assertEquals(expected, actual) + check(expected == actual) } } - @Test - fun batching() = runTest { + test("Batching") { val initial = createCache() @Suppress("RemoveExplicitTypeArguments") // IDEA is wrong, they are necessary @@ -167,7 +168,8 @@ class ContextualCacheTest { run { val expected = (0 until 200 step 2).toList().successful() val actual = cache[Identifier(even = true, odd = false), Context(0, 100)].now() - assertEquals(expected, actual) + check(expected == actual) } } -} + +}) diff --git a/logger/build.gradle.kts b/logger/build.gradle.kts index 2145d67..f8b9029 100644 --- a/logger/build.gradle.kts +++ b/logger/build.gradle.kts @@ -22,7 +22,7 @@ kotlin { val commonMain by getting val commonTest by getting { dependencies { - implementation(projects.tester) + implementation(libs.bundles.prepared) } } diff --git a/logger/src/commonTest/kotlin/LoggerTest.kt b/logger/src/commonTest/kotlin/LoggerTest.kt index d98fc62..d110faa 100644 --- a/logger/src/commonTest/kotlin/LoggerTest.kt +++ b/logger/src/commonTest/kotlin/LoggerTest.kt @@ -5,12 +5,11 @@ import opensavvy.logger.Logger.Companion.error import opensavvy.logger.Logger.Companion.info import opensavvy.logger.Logger.Companion.trace import opensavvy.logger.Logger.Companion.warn -import kotlin.test.Test +import opensavvy.prepared.runner.kotest.PreparedSpec -class LoggerTest { +class LoggerTest : PreparedSpec({ - @Test - fun output() { + test("Output") { val log = loggerFor(this) log.level = LogLevel.TRACE @@ -21,10 +20,9 @@ class LoggerTest { log.error { "This is an error!" } } - private data class Message(val int: Int, val text: String) + data class Message(val int: Int, val text: String) - @Test - fun outputWithObject() { + test("Output with object") { val message = Message(5, "hello") val log = loggerFor(this) @@ -36,4 +34,4 @@ class LoggerTest { log.warn(message) { "This is a warning!" } log.error(message) { "This is an error!" } } -} +}) diff --git a/progress-coroutines/build.gradle.kts b/progress-coroutines/build.gradle.kts index 50d744d..466ae40 100644 --- a/progress-coroutines/build.gradle.kts +++ b/progress-coroutines/build.gradle.kts @@ -22,8 +22,6 @@ kotlin { } sourceSets.commonTest.dependencies { - implementation(projects.tester) - implementation(libs.kotlinx.coroutines.test) implementation(libs.bundles.prepared) } } diff --git a/progress-coroutines/src/commonTest/kotlin/CoroutineProgressReporterTest.kt b/progress-coroutines/src/commonTest/kotlin/CoroutineProgressReporterTest.kt index a7bbe19..32826dd 100644 --- a/progress-coroutines/src/commonTest/kotlin/CoroutineProgressReporterTest.kt +++ b/progress-coroutines/src/commonTest/kotlin/CoroutineProgressReporterTest.kt @@ -1,19 +1,16 @@ package opensavvy.progress.coroutines import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withContext +import opensavvy.prepared.runner.kotest.PreparedSpec import opensavvy.progress.Progress import opensavvy.progress.loading import opensavvy.progress.report.ProgressReporter -import kotlin.test.Test -import kotlin.test.assertEquals @OptIn(ExperimentalCoroutinesApi::class) -class CoroutineProgressReporterTest { +class CoroutineProgressReporterTest : PreparedSpec({ - @Test - fun report() = runTest { + test("Report a value through the coroutine context") { var value: Progress? = null val reporter = ProgressReporter { value = it } @@ -23,22 +20,21 @@ class CoroutineProgressReporterTest { report(loading(0.2)) } - assertEquals(loading(0.2), value) + check(value == loading(0.2)) } - @Test - fun reportWithoutReporter() = runTest { + test("Report without a reporter should do nothing") { report(loading(0.2)) // does nothing, doesn't crash } - @Test - fun withReporter() = runTest { + test("Report a value using a callback") { var value: Progress? = null reportProgress({ value = it }) { report(loading(0.2)) } - assertEquals(loading(0.2), value) + check(value == loading(0.2)) } -} + +}) diff --git a/progress-coroutines/src/commonTest/kotlin/ProgressReportTransformerTest.kt b/progress-coroutines/src/commonTest/kotlin/ProgressReportTransformerTest.kt index 038f2db..6f1aed9 100644 --- a/progress-coroutines/src/commonTest/kotlin/ProgressReportTransformerTest.kt +++ b/progress-coroutines/src/commonTest/kotlin/ProgressReportTransformerTest.kt @@ -1,44 +1,42 @@ package opensavvy.progress.coroutines import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withContext +import opensavvy.prepared.runner.kotest.PreparedSpec import opensavvy.progress.Progress import opensavvy.progress.loading import opensavvy.progress.report.ProgressReporter -import kotlin.test.Test -import kotlin.test.assertEquals @OptIn(ExperimentalCoroutinesApi::class) -class ProgressReportTransformerTest { +class ProgressReportTransformerTest : PreparedSpec({ - @Test - fun test() = runTest { + test("Using the progress transformer") { var value: Progress? = null withContext(ProgressReporter { value = it }.asCoroutineContext()) { report(loading(0.1)) - assertEquals(loading(0.1), value) + check(value == loading(0.1)) mapProgressTo(0.2..0.5) { report(loading(0.0)) - assertEquals(loading(0.2), value) + check(value == loading(0.2)) report(loading(0.5)) - assertEquals(loading(0.35), value) + check(value == loading(0.35)) report(loading(1.0)) - assertEquals(loading(0.5), value) + check(value == loading(0.5)) } report(loading(0.9)) - assertEquals(loading(0.9), value) + check(value == loading(0.9)) } // Calling the function without reporter should no-op, NOT fail mapProgressTo(0.2..0.5) { report(loading(0.0)) - assertEquals(loading(0.9), value) // The value shouldn't be impacted, since we should no-op + check(value == loading(0.9)) // The value shouldn't be impacted, since we should no-op } } -} + +}) diff --git a/progress-coroutines/src/commonTest/kotlin/StateFlowProgressReporterTest.kt b/progress-coroutines/src/commonTest/kotlin/StateFlowProgressReporterTest.kt index fa32044..6e42cf3 100644 --- a/progress-coroutines/src/commonTest/kotlin/StateFlowProgressReporterTest.kt +++ b/progress-coroutines/src/commonTest/kotlin/StateFlowProgressReporterTest.kt @@ -3,19 +3,16 @@ package opensavvy.progress.coroutines import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.transformWhile -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.runTest import kotlinx.coroutines.yield +import opensavvy.prepared.runner.kotest.PreparedSpec +import opensavvy.prepared.suite.launch import opensavvy.progress.done import opensavvy.progress.loading -import kotlin.test.Test -import kotlin.test.assertEquals @OptIn(ExperimentalCoroutinesApi::class) -class StateFlowProgressReporterTest { +class StateFlowProgressReporterTest : PreparedSpec({ - @Test - fun test() = runTest { + test("Reporting progress events through a StateFlow") { val reporter = StateFlowProgressReporter() launch { @@ -25,7 +22,7 @@ class StateFlowProgressReporterTest { reporter.report(loading(0.3)) yield() - assertEquals("StateFlowProgressReporter(progress=Loading(30%))", reporter.toString()) + check(reporter.toString() == "StateFlowProgressReporter(progress=Loading(30%))") reporter.report(done()) yield() @@ -38,6 +35,7 @@ class StateFlowProgressReporterTest { done(), ) - assertEquals(expected, reporter.progress.transformWhile { emit(it); it != done() }.toList()) + check(expected == reporter.progress.transformWhile { emit(it); it != done() }.toList()) } -} + +}) diff --git a/progress/build.gradle.kts b/progress/build.gradle.kts index 4e43ded..9e47205 100644 --- a/progress/build.gradle.kts +++ b/progress/build.gradle.kts @@ -19,7 +19,6 @@ kotlin { } sourceSets.commonTest.dependencies { - implementation(projects.tester) implementation(libs.bundles.prepared) } } diff --git a/progress/src/commonTest/kotlin/SimpleLoadingImplementationTest.kt b/progress/src/commonTest/kotlin/SimpleLoadingImplementationTest.kt index 4be69f6..b2a14c0 100644 --- a/progress/src/commonTest/kotlin/SimpleLoadingImplementationTest.kt +++ b/progress/src/commonTest/kotlin/SimpleLoadingImplementationTest.kt @@ -6,7 +6,6 @@ import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import opensavvy.prepared.runner.kotest.PreparedSpec -import kotlin.test.assertContains @Suppress("unused") class SimpleLoadingImplementationTest : PreparedSpec({ @@ -71,11 +70,11 @@ class SimpleLoadingImplementationTest : PreparedSpec({ loading(1.0), ) - assertContains(set, done()) - assertContains(set, loading(0.0)) - assertContains(set, loading(0.7)) - assertContains(set, loading(0.9)) - assertContains(set, loading(1.0)) + check(done() in set) + check(loading(0.0) in set) + check(loading(0.7) in set) + check(loading(0.9) in set) + check(loading(1.0) in set) } suite("Equality") { diff --git a/progress/src/commonTest/kotlin/report/MapProgressReporterTest.kt b/progress/src/commonTest/kotlin/report/MapProgressReporterTest.kt index 2b94c0f..8ec3203 100644 --- a/progress/src/commonTest/kotlin/report/MapProgressReporterTest.kt +++ b/progress/src/commonTest/kotlin/report/MapProgressReporterTest.kt @@ -5,7 +5,6 @@ import opensavvy.prepared.runner.kotest.PreparedSpec import opensavvy.progress.Progress import opensavvy.progress.done import opensavvy.progress.loading -import kotlin.test.assertEquals @Suppress("unused") class MapProgressReporterTest : PreparedSpec({ @@ -35,6 +34,6 @@ class MapProgressReporterTest : PreparedSpec({ .reduceToInterval(0.1, 0.2) .map { it } - assertEquals("NoOpProgressReporter.reduceToInterval(0.1..0.2).map()", reporter.toString()) + check(reporter.toString() == "NoOpProgressReporter.reduceToInterval(0.1..0.2).map()") } }) diff --git a/settings.gradle.kts b/settings.gradle.kts index c54acf6..217a821 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -70,6 +70,5 @@ include( "weak", - "logger", - "tester", + "logger" ) diff --git a/state-arrow/build.gradle.kts b/state-arrow/build.gradle.kts index b830dcc..8e446ab 100644 --- a/state-arrow/build.gradle.kts +++ b/state-arrow/build.gradle.kts @@ -22,7 +22,6 @@ kotlin { } sourceSets.commonTest.dependencies { - implementation(projects.tester) } } diff --git a/state-coroutines/build.gradle.kts b/state-coroutines/build.gradle.kts index 85d6978..e0edafe 100644 --- a/state-coroutines/build.gradle.kts +++ b/state-coroutines/build.gradle.kts @@ -26,9 +26,6 @@ kotlin { sourceSets.commonTest.dependencies { implementation(libs.bundles.prepared) implementation(projects.stateArrow) - - api(libs.kotlinx.coroutines.test) - implementation(projects.tester) } } diff --git a/state-coroutines/src/commonTest/kotlin/AccumulatorsTest.kt b/state-coroutines/src/commonTest/kotlin/AccumulatorsTest.kt index db4073e..994d318 100644 --- a/state-coroutines/src/commonTest/kotlin/AccumulatorsTest.kt +++ b/state-coroutines/src/commonTest/kotlin/AccumulatorsTest.kt @@ -6,7 +6,6 @@ import opensavvy.prepared.runner.kotest.PreparedSpec import opensavvy.progress.loading import opensavvy.state.progressive.ProgressiveOutcome import opensavvy.state.progressive.combineCompleted -import kotlin.test.assertEquals class AccumulatorsTest : PreparedSpec({ @@ -35,6 +34,6 @@ class AccumulatorsTest : PreparedSpec({ val actual = initial.asFlow().combineCompleted().toList() - assertEquals(expected, actual) + check(actual == expected) } }) diff --git a/state-coroutines/src/commonTest/kotlin/CaptureProgressTest.kt b/state-coroutines/src/commonTest/kotlin/CaptureProgressTest.kt index 8a34bfa..82c6d7e 100644 --- a/state-coroutines/src/commonTest/kotlin/CaptureProgressTest.kt +++ b/state-coroutines/src/commonTest/kotlin/CaptureProgressTest.kt @@ -7,7 +7,6 @@ import opensavvy.progress.coroutines.report import opensavvy.progress.loading import opensavvy.state.outcome.Outcome import opensavvy.state.progressive.ProgressiveOutcome -import kotlin.test.assertEquals private object Error @@ -31,7 +30,7 @@ class CaptureProgressTest : PreparedSpec({ ProgressiveOutcome.Success(5), ) - assertEquals(expect, actual) + check(expect == actual) } test("captureFromBlock") { @@ -47,6 +46,6 @@ class CaptureProgressTest : PreparedSpec({ ProgressiveOutcome.Success(5), ) - assertEquals(expect, actual) + check(expect == actual) } }) diff --git a/state-coroutines/src/commonTest/kotlin/ProgressiveFlowAccessorsTest.kt b/state-coroutines/src/commonTest/kotlin/ProgressiveFlowAccessorsTest.kt index af7db8e..5659e5a 100644 --- a/state-coroutines/src/commonTest/kotlin/ProgressiveFlowAccessorsTest.kt +++ b/state-coroutines/src/commonTest/kotlin/ProgressiveFlowAccessorsTest.kt @@ -5,7 +5,6 @@ import opensavvy.prepared.runner.kotest.PreparedSpec import opensavvy.progress.loading import opensavvy.state.outcome.Outcome import opensavvy.state.progressive.ProgressiveOutcome -import kotlin.test.assertEquals class ProgressiveFlowAccessorsTest : PreparedSpec({ @@ -18,10 +17,7 @@ class ProgressiveFlowAccessorsTest : PreparedSpec({ ProgressiveOutcome.Failure(NotFound(2)), ) - assertEquals( - Outcome.Failure(NotFound(2)), - input.now(), - ) + check(input.now() == Outcome.Failure(NotFound(2))) } test("Success") { @@ -31,9 +27,6 @@ class ProgressiveFlowAccessorsTest : PreparedSpec({ ProgressiveOutcome.Success(2), ) - assertEquals( - Outcome.Success(2), - input.now(), - ) + check(input.now() == Outcome.Success(2)) } }) diff --git a/state/build.gradle.kts b/state/build.gradle.kts index 43f1dc8..5a9ddfb 100644 --- a/state/build.gradle.kts +++ b/state/build.gradle.kts @@ -25,9 +25,6 @@ kotlin { sourceSets.commonTest.dependencies { implementation(libs.bundles.prepared) implementation(projects.stateArrow) - - api(libs.kotlinx.coroutines.test) - implementation(projects.tester) } } diff --git a/state/src/commonTest/kotlin/ProgressionTest.kt b/state/src/commonTest/kotlin/ProgressionTest.kt index 6f00273..11b9d3e 100644 --- a/state/src/commonTest/kotlin/ProgressionTest.kt +++ b/state/src/commonTest/kotlin/ProgressionTest.kt @@ -2,36 +2,35 @@ package opensavvy.state import com.benwoodworth.parameterize.parameterOf import com.benwoodworth.parameterize.parameterize +import io.kotest.assertions.throwables.shouldThrow import opensavvy.prepared.runner.kotest.PreparedSpec import opensavvy.progress.loading -import kotlin.test.assertEquals -import kotlin.test.assertFails class ProgressionTest : PreparedSpec({ test("String representation of loading states") { - assertEquals("Loading", loading().toString()) - assertEquals("Loading(0%)", loading(0.0).toString()) - assertEquals("Loading(33%)", loading(0.33).toString()) - assertEquals("Loading(100%)", loading(1.0).toString()) + check(loading().toString() == "Loading") + check(loading(0.0).toString() == "Loading(0%)") + check(loading(0.33).toString() == "Loading(33%)") + check(loading(1.0).toString() == "Loading(100%)") } test("0% loading") { val start = loading(0.0) - assertEquals(0.0, start.normalized) - assertEquals(0, start.percent) + check(start.normalized == 0.0) + check(start.percent == 0) } test("33% loading") { val third = loading(1.0 / 3) - assertEquals(0.3333333333333333, third.normalized) - assertEquals(33, third.percent) + check(third.normalized == 0.3333333333333333) + check(third.percent == 33) } test("100% loading") { val end = loading(1.0) - assertEquals(1.0, end.normalized) - assertEquals(100, end.percent) + check(end.normalized == 1.0) + check(end.percent == 100) } suite("Illegal progression values") { @@ -39,7 +38,7 @@ class ProgressionTest : PreparedSpec({ val parameter by parameterOf(-1.0, 1.01, 1.00000001, Double.MAX_VALUE, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY) test("A progress value of $parameter is not allowed") { - assertFails { + shouldThrow { loading(parameter) } } diff --git a/state/src/commonTest/kotlin/arrow/ConverterTest.kt b/state/src/commonTest/kotlin/arrow/ConverterTest.kt index 3368cfd..2a0ae41 100644 --- a/state/src/commonTest/kotlin/arrow/ConverterTest.kt +++ b/state/src/commonTest/kotlin/arrow/ConverterTest.kt @@ -10,7 +10,6 @@ import opensavvy.state.progressive.ProgressiveOutcome import opensavvy.state.progressive.failedWithProgress import opensavvy.state.progressive.successfulWithProgress import opensavvy.state.progressive.withProgress -import kotlin.test.assertEquals class ConverterTest : PreparedSpec({ @@ -20,40 +19,25 @@ class ConverterTest : PreparedSpec({ suite("Outcome") { test("Success") { - assertEquals( - 5.right(), - 5.successful().toEither() - ) + check(5.successful().toEither() == 5.right()) } test("Failure") { - assertEquals( - NotFound(5).left(), - NotFound(5).failed().toEither() - ) + check(NotFound(5).failed().toEither() == NotFound(5).left()) } } suite("ProgressiveOutcome") { test("Success") { - assertEquals( - 5.right(), - 5.successful().withProgress(loading(0.2)).toEither() - ) + check(5.successful().withProgress(loading(0.2)).toEither() == 5.right()) } test("Failure") { - assertEquals( - NotFound(5).left(), - NotFound(5).failedWithProgress().toEither(), - ) + check(NotFound(5).failedWithProgress().toEither() == NotFound(5).left()) } test("Incomplete") { - assertEquals( - null, - ProgressiveOutcome.Incomplete().toEither(), - ) + check(ProgressiveOutcome.Incomplete().toEither() == null) } } } @@ -61,33 +45,21 @@ class ConverterTest : PreparedSpec({ suite("From either") { suite("Outcome") { test("Success") { - assertEquals( - 5.successful(), - 5.right().toOutcome() - ) + check(5.right().toOutcome() == 5.successful()) } test("Failure") { - assertEquals( - NotFound(5).failed(), - NotFound(5).left().toOutcome() - ) + check(NotFound(5).left().toOutcome() == NotFound(5).failed()) } } suite("ProgressiveOutcome") { suite("Success") { - assertEquals( - 5.successfulWithProgress(progress = loading(0.5)), - 5.right().toOutcome(progress = loading(0.5)), - ) + check(5.right().toOutcome(loading(0.5)) == 5.successfulWithProgress(loading(0.5))) } suite("Failure") { - assertEquals( - NotFound(5).failedWithProgress(progress = loading(0.5)), - NotFound(5).left().toOutcome(progress = loading(0.5)), - ) + check(NotFound(5).left().toOutcome(loading(0.5)) == NotFound(5).failedWithProgress(loading(0.5))) } } } diff --git a/state/src/commonTest/kotlin/progressive/AccessorsTest.kt b/state/src/commonTest/kotlin/progressive/AccessorsTest.kt index a99cdc5..d6b3f35 100644 --- a/state/src/commonTest/kotlin/progressive/AccessorsTest.kt +++ b/state/src/commonTest/kotlin/progressive/AccessorsTest.kt @@ -3,8 +3,8 @@ package opensavvy.state.progressive import opensavvy.prepared.runner.kotest.PreparedSpec import opensavvy.progress.loading import opensavvy.state.ExperimentalProgressiveRaiseApi -import opensavvy.state.outcome.Outcome -import kotlin.test.assertEquals +import opensavvy.state.outcome.failed +import opensavvy.state.outcome.successful class AccessorsTest : PreparedSpec({ @@ -15,18 +15,12 @@ class AccessorsTest : PreparedSpec({ suite("Access progress of unsuccessful values") { test("Failure") { val failure = ProgressiveOutcome.Failure(Unit, loading(0.13)) as ProgressiveOutcome.Unsuccessful - assertEquals( - loading(0.13), - failure.progress - ) + check(failure.progress == loading(0.13)) } test("Incomplete") { val failure = ProgressiveOutcome.Incomplete(loading(0.13)) as ProgressiveOutcome.Unsuccessful - assertEquals( - loading(0.13), - failure.progress - ) + check(failure.progress == loading(0.13)) } } @@ -67,24 +61,24 @@ class AccessorsTest : PreparedSpec({ val value: ProgressiveOutcome<*, Int> = ProgressiveOutcome.Success(5, loading(0.23)) val (outcome, progress) = value - assertEquals(Outcome.Success(5), outcome) - assertEquals(loading(0.23), progress) + check(outcome == 5.successful()) + check(progress == loading(0.23)) } test("Failure") { val value: ProgressiveOutcome<*, Int> = ProgressiveOutcome.Failure(Failed, loading(0.23)) val (outcome, progress) = value - assertEquals(Outcome.Failure(Failed), outcome) - assertEquals(loading(0.23), progress) + check(outcome == Failed.failed()) + check(progress == loading(0.23)) } test("Incomplete") { val value: ProgressiveOutcome<*, Int> = ProgressiveOutcome.Incomplete(loading(0.23)) val (outcome, progress) = value - assertEquals(null, outcome) - assertEquals(loading(0.23), progress) + check(outcome == null) + check(progress == loading(0.23)) } } }) diff --git a/state/src/commonTest/kotlin/progressive/AccumulatorsTest.kt b/state/src/commonTest/kotlin/progressive/AccumulatorsTest.kt index 9f2042d..4335325 100644 --- a/state/src/commonTest/kotlin/progressive/AccumulatorsTest.kt +++ b/state/src/commonTest/kotlin/progressive/AccumulatorsTest.kt @@ -2,7 +2,6 @@ package opensavvy.state.progressive import opensavvy.prepared.runner.kotest.PreparedSpec import opensavvy.progress.loading -import kotlin.test.assertEquals class AccumulatorsTest : PreparedSpec({ @@ -30,7 +29,7 @@ class AccumulatorsTest : PreparedSpec({ ProgressiveOutcome.Failure(Unit, loading(0.3)), ) - assertEquals(expected, actual) + check(actual == expected) } test("Sequence") { @@ -58,6 +57,6 @@ class AccumulatorsTest : PreparedSpec({ ProgressiveOutcome.Failure(Unit, loading(0.3)), ) - assertEquals(expected, actual) + check(actual == expected) } }) diff --git a/state/src/commonTest/kotlin/progressive/BuilderTest.kt b/state/src/commonTest/kotlin/progressive/BuilderTest.kt index ca4b2fa..bdaf04d 100644 --- a/state/src/commonTest/kotlin/progressive/BuilderTest.kt +++ b/state/src/commonTest/kotlin/progressive/BuilderTest.kt @@ -7,7 +7,6 @@ import opensavvy.progress.loading import opensavvy.state.outcome.Outcome import opensavvy.state.outcome.failed import opensavvy.state.outcome.successful -import kotlin.test.assertEquals @OptIn(ExperimentalProgressApi::class) class BuilderTest : PreparedSpec({ @@ -17,17 +16,11 @@ class BuilderTest : PreparedSpec({ suite("With progress") { test("Success") { - assertEquals( - ProgressiveOutcome.Success(5, loading(0.57)), - Outcome.Success(5).withProgress(loading(0.57)), - ) + check(Outcome.Success(5).withProgress(loading(0.57)) == ProgressiveOutcome.Success(5, loading(0.57))) } test("Failure") { - assertEquals( - ProgressiveOutcome.Failure(Failed, loading(0.57)), - Outcome.Failure(Failed).withProgress(loading(0.57)), - ) + check(Outcome.Failure(Failed).withProgress(loading(0.57)) == ProgressiveOutcome.Failure(Failed, loading(0.57))) } } @@ -35,26 +28,17 @@ class BuilderTest : PreparedSpec({ test("Success") { val initial = Progressive(5.successful(), loading(0.2)) - assertEquals( - initial, - initial.flatten().explode(), - ) + check(initial == initial.flatten().explode()) } test("Failure") { val initial = Progressive(3.failed(), loading(0.1)) - assertEquals( - initial, - initial.flatten().explode(), - ) + check(initial == initial.flatten().explode()) } test("Incomplete") { - assertEquals( - Progressive(null, loading(0.33)), - ProgressiveOutcome.Incomplete(loading(0.33)).explode(), - ) + check(ProgressiveOutcome.Incomplete(loading(0.33)).explode() == Progressive(null, loading(0.33))) } } @@ -64,24 +48,15 @@ class BuilderTest : PreparedSpec({ copy(progress = loading(0.23)) test("Success") { - assertEquals( - ProgressiveOutcome.Success(Unit, loading(0.23)), - ProgressiveOutcome.Success(Unit).copyProgress() - ) + check(ProgressiveOutcome.Success(Unit).copyProgress() == ProgressiveOutcome.Success(Unit, loading(0.23))) } test("Failure") { - assertEquals( - ProgressiveOutcome.Failure(Unit, loading(0.23)), - ProgressiveOutcome.Failure(Unit).copyProgress() - ) + check(ProgressiveOutcome.Failure(Unit).copyProgress() == ProgressiveOutcome.Failure(Unit, loading(0.23))) } test("Incomplete") { - assertEquals( - ProgressiveOutcome.Incomplete(loading(0.23)), - ProgressiveOutcome.Incomplete().copyProgress() - ) + check(ProgressiveOutcome.Incomplete().copyProgress() == ProgressiveOutcome.Incomplete(loading(0.23))) } } }) diff --git a/state/src/commonTest/kotlin/progressive/ConditionalTest.kt b/state/src/commonTest/kotlin/progressive/ConditionalTest.kt index c688551..69585f7 100644 --- a/state/src/commonTest/kotlin/progressive/ConditionalTest.kt +++ b/state/src/commonTest/kotlin/progressive/ConditionalTest.kt @@ -2,8 +2,6 @@ package opensavvy.state.progressive import opensavvy.prepared.runner.kotest.PreparedSpec import opensavvy.progress.loading -import kotlin.test.assertFalse -import kotlin.test.assertTrue class ConditionalTest : PreparedSpec({ @@ -18,7 +16,7 @@ class ConditionalTest : PreparedSpec({ test = true } - assertTrue(test) + check(test) } test("Failure") { @@ -28,7 +26,7 @@ class ConditionalTest : PreparedSpec({ test = true } - assertFalse(test) + check(!test) } test("Incomplete") { @@ -38,7 +36,7 @@ class ConditionalTest : PreparedSpec({ test = true } - assertFalse(test) + check(!test) } } @@ -50,7 +48,7 @@ class ConditionalTest : PreparedSpec({ test = true } - assertFalse(test) + check(!test) } test("Failure") { @@ -60,7 +58,7 @@ class ConditionalTest : PreparedSpec({ test = true } - assertTrue(test) + check(test) } test("Incomplete") { @@ -70,7 +68,7 @@ class ConditionalTest : PreparedSpec({ test = true } - assertFalse(test) + check(!test) } } @@ -82,7 +80,7 @@ class ConditionalTest : PreparedSpec({ test = true } - assertFalse(test) + check(!test) } test("Failure") { @@ -92,7 +90,7 @@ class ConditionalTest : PreparedSpec({ test = true } - assertFalse(test) + check(!test) } test("Incomplete") { @@ -102,7 +100,7 @@ class ConditionalTest : PreparedSpec({ test = true } - assertTrue(test) + check(test) } } @@ -114,7 +112,7 @@ class ConditionalTest : PreparedSpec({ test = true } - assertFalse(test) + check(!test) } test("Loading") { @@ -124,7 +122,7 @@ class ConditionalTest : PreparedSpec({ test = true } - assertTrue(test) + check(test) } test("Incomplete") { @@ -134,7 +132,7 @@ class ConditionalTest : PreparedSpec({ test = true } - assertTrue(test) + check(test) } } }) diff --git a/state/src/commonTest/kotlin/progressive/MappingTest.kt b/state/src/commonTest/kotlin/progressive/MappingTest.kt index 5e696b1..5caaa85 100644 --- a/state/src/commonTest/kotlin/progressive/MappingTest.kt +++ b/state/src/commonTest/kotlin/progressive/MappingTest.kt @@ -1,7 +1,6 @@ package opensavvy.state.progressive import opensavvy.prepared.runner.kotest.PreparedSpec -import kotlin.test.assertEquals class MappingTest : PreparedSpec({ @@ -9,53 +8,37 @@ class MappingTest : PreparedSpec({ suite("map") { test("Success") { - assertEquals( - ProgressiveOutcome.Success("5"), - ProgressiveOutcome.Success(5).map { it.toString() }, - ) + check(ProgressiveOutcome.Success(5).map { it.toString() } == "5".successfulWithProgress()) } @Suppress("UNREACHABLE_CODE") // it's the purpose of the test! test("Failure") { - assertEquals( - ProgressiveOutcome.Failure(Failed), - ProgressiveOutcome.Failure(Failed).map { it.toString() }, - ) + check(ProgressiveOutcome.Failure(Failed).map { it.toString() } == Failed.failedWithProgress()) } @Suppress("UNREACHABLE_CODE") // it's the purpose of the test! test("Incomplete") { - assertEquals( - ProgressiveOutcome.Incomplete(), - ProgressiveOutcome.Incomplete().map { it.toString() }, - ) + check(ProgressiveOutcome.Incomplete().map { it.toString() } == ProgressiveOutcome.Incomplete()) } } suite("mapFailure") { test("Success") { - assertEquals( - ProgressiveOutcome.Success(5), + check( ProgressiveOutcome.Success(5).mapFailure { @Suppress("UNREACHABLE_CODE") // it's the purpose of the test! it.toString() - }, + } == 5.successfulWithProgress() ) } test("Failure") { - assertEquals( - ProgressiveOutcome.Failure("5"), - ProgressiveOutcome.Failure(5).mapFailure { it.toString() }, - ) + check(ProgressiveOutcome.Failure(5).mapFailure { it.toString() } == "5".failedWithProgress()) } @Suppress("UNREACHABLE_CODE") // it's the purpose of the test! test("Incomplete") { - assertEquals( - ProgressiveOutcome.Incomplete(), - ProgressiveOutcome.Incomplete().mapFailure { it.toString() }, - ) + check(ProgressiveOutcome.Incomplete().mapFailure { it.toString() } == ProgressiveOutcome.Incomplete()) } } }) diff --git a/tester/README.md b/tester/README.md deleted file mode 100644 index c914040..0000000 --- a/tester/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Tester - -Small convenience module to store common utilities for testing. diff --git a/tester/build.gradle.kts b/tester/build.gradle.kts deleted file mode 100644 index 377c2e3..0000000 --- a/tester/build.gradle.kts +++ /dev/null @@ -1,37 +0,0 @@ -plugins { - alias(opensavvyConventions.plugins.base) - alias(opensavvyConventions.plugins.kotlin.internal) -} - -kotlin { - jvm() - js(IR) { - browser() - nodejs() - } - iosSimulatorArm64() - iosArm64() - iosX64() - linuxX64() - - sourceSets { - val commonMain by getting { - dependencies { - api(kotlin("test-common")) - api(kotlin("test-annotations-common")) - } - } - - val jvmMain by getting { - dependencies { - api(kotlin("test-junit5")) - } - } - - val jsMain by getting { - dependencies { - api(kotlin("test-js")) - } - } - } -} diff --git a/tester/src/commonMain/kotlin/Marker.kt b/tester/src/commonMain/kotlin/Marker.kt deleted file mode 100644 index 4578bd4..0000000 --- a/tester/src/commonMain/kotlin/Marker.kt +++ /dev/null @@ -1,6 +0,0 @@ -package opensavvy.tester - -// Yes, this is useless, -// but iOS doesn't like empty modules. -@Suppress("unused") -private typealias Marker = Nothing diff --git a/tester/src/jvmMain/resources/logback.xml b/tester/src/jvmMain/resources/logback.xml deleted file mode 100644 index 2f8b213..0000000 --- a/tester/src/jvmMain/resources/logback.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - %d{HH:mm:ss.SSS} %-5level %-50logger{40}\t%msg%n - - - - - - - - -- 2.51.2 From 40a906aafb9c25b76dd4d281fae530e050290410 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Fri, 20 Dec 2024 15:29:42 +0100 Subject: [PATCH 4/8] breaking: Remove state-arrow, which has already been migrated to state --- backbone/build.gradle.kts | 1 - build.gradle.kts | 1 - cache-blocking/build.gradle.kts | 1 - cache/build.gradle.kts | 1 - settings.gradle.kts | 1 - state-arrow/README.md | 9 ------ state-arrow/build.gradle.kts | 37 ---------------------- state-arrow/src/commonMain/kotlin/Empty.kt | 9 ------ state-coroutines/build.gradle.kts | 1 - state/build.gradle.kts | 1 - 10 files changed, 62 deletions(-) delete mode 100644 state-arrow/README.md delete mode 100644 state-arrow/build.gradle.kts delete mode 100644 state-arrow/src/commonMain/kotlin/Empty.kt diff --git a/backbone/build.gradle.kts b/backbone/build.gradle.kts index 5b4d460..c04e234 100644 --- a/backbone/build.gradle.kts +++ b/backbone/build.gradle.kts @@ -30,7 +30,6 @@ kotlin { val commonTest by getting { dependencies { implementation(libs.bundles.prepared) - implementation(projects.stateArrow) } } } diff --git a/build.gradle.kts b/build.gradle.kts index 934129f..a5d2a8e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -28,7 +28,6 @@ dependencies { dokkatoo(projects.progress) dokkatoo(projects.progressCoroutines) dokkatoo(projects.state) - dokkatoo(projects.stateArrow) dokkatoo(projects.stateCoroutines) dokkatoo(projects.weak) diff --git a/cache-blocking/build.gradle.kts b/cache-blocking/build.gradle.kts index 36d52eb..9b2155f 100644 --- a/cache-blocking/build.gradle.kts +++ b/cache-blocking/build.gradle.kts @@ -22,7 +22,6 @@ kotlin { implementation(libs.bundles.prepared) implementation(libs.lincheck) implementation(opensavvyConventions.aligned.kotlin.test) // Needed by Lincheck - implementation(projects.stateArrow) } } } diff --git a/cache/build.gradle.kts b/cache/build.gradle.kts index 04e5c2d..108bd75 100644 --- a/cache/build.gradle.kts +++ b/cache/build.gradle.kts @@ -23,7 +23,6 @@ kotlin { sourceSets.commonTest.dependencies { implementation(libs.bundles.prepared) - implementation(projects.stateArrow) } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 217a821..50dbfa0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -60,7 +60,6 @@ include( "progress-coroutines", "state", - "state-arrow", "state-coroutines", "cache", diff --git a/state-arrow/README.md b/state-arrow/README.md deleted file mode 100644 index c9f7e49..0000000 --- a/state-arrow/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Module State (Arrow compatibility, deprecated) - -Compatibility layer for `state` and [Arrow Core](https://arrow-kt.io/learn/typed-errors/working-with-typed-errors/). - - - - - -All functionality from this module has been migrated into the `state` module. Please delete this module from your dependencies, it will be removed in Pedestal 3.0. diff --git a/state-arrow/build.gradle.kts b/state-arrow/build.gradle.kts deleted file mode 100644 index 8e446ab..0000000 --- a/state-arrow/build.gradle.kts +++ /dev/null @@ -1,37 +0,0 @@ -plugins { - alias(opensavvyConventions.plugins.base) - alias(opensavvyConventions.plugins.kotlin.library) -} - -kotlin { - jvm() - js(IR) { - browser() - nodejs() - } - iosSimulatorArm64() - iosArm64() - iosX64() - linuxX64() - - sourceSets.commonMain.dependencies { - api(projects.state) - api(libs.arrow.core) - - implementation(projects.logger) - } - - sourceSets.commonTest.dependencies { - } -} - -library { - name.set("State (Arrow compatibility, deprecated)") - description.set("Progress-aware failure states") - homeUrl.set("https://opensavvy.gitlab.io/groundwork/pedestal/api-docs/state-arrow/index.html") - - license.set { - name.set("Apache 2.0") - url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") - } -} diff --git a/state-arrow/src/commonMain/kotlin/Empty.kt b/state-arrow/src/commonMain/kotlin/Empty.kt deleted file mode 100644 index 48399af..0000000 --- a/state-arrow/src/commonMain/kotlin/Empty.kt +++ /dev/null @@ -1,9 +0,0 @@ -package opensavvy.state.arrow - -/** - * All functionality from this module has been merged into the 'state' module. - * - * You may remove this module from your dependencies. - */ -@Deprecated("Will be removed in Pedestal 3.0") -typealias Nothing = kotlin.Nothing diff --git a/state-coroutines/build.gradle.kts b/state-coroutines/build.gradle.kts index e0edafe..fb90762 100644 --- a/state-coroutines/build.gradle.kts +++ b/state-coroutines/build.gradle.kts @@ -25,7 +25,6 @@ kotlin { sourceSets.commonTest.dependencies { implementation(libs.bundles.prepared) - implementation(projects.stateArrow) } } diff --git a/state/build.gradle.kts b/state/build.gradle.kts index 5a9ddfb..8febaaa 100644 --- a/state/build.gradle.kts +++ b/state/build.gradle.kts @@ -24,7 +24,6 @@ kotlin { sourceSets.commonTest.dependencies { implementation(libs.bundles.prepared) - implementation(projects.stateArrow) } } -- 2.51.2 From 4057d8471a65c45e012a963582b4339d976f1d3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Fri, 20 Dec 2024 15:33:09 +0100 Subject: [PATCH 5/8] breaking(cache): Remove deprecated methods See #141 --- cache/src/commonMain/kotlin/ExpirationCache.kt | 5 ----- .../kotlin/contextual/ContextualExpirationCache.kt | 5 ----- 2 files changed, 10 deletions(-) diff --git a/cache/src/commonMain/kotlin/ExpirationCache.kt b/cache/src/commonMain/kotlin/ExpirationCache.kt index 130d0c3..5bb951d 100644 --- a/cache/src/commonMain/kotlin/ExpirationCache.kt +++ b/cache/src/commonMain/kotlin/ExpirationCache.kt @@ -136,8 +136,3 @@ internal class ExpirationCache( */ fun Cache.expireAfter(duration: Duration, scope: CoroutineScope, clock: Clock): Cache = ExpirationCache(this, duration, clock, scope) - -@Suppress("DeprecatedCallableAddReplaceWith") -@Deprecated(message = "Specifying the clock explicitly will become mandatory in 2.0.") -fun Cache.expireAfter(duration: Duration, scope: CoroutineScope): Cache = - ExpirationCache(this, duration, Clock.System, scope) diff --git a/cache/src/commonMain/kotlin/contextual/ContextualExpirationCache.kt b/cache/src/commonMain/kotlin/contextual/ContextualExpirationCache.kt index 63c91cc..ef77f97 100644 --- a/cache/src/commonMain/kotlin/contextual/ContextualExpirationCache.kt +++ b/cache/src/commonMain/kotlin/contextual/ContextualExpirationCache.kt @@ -46,8 +46,3 @@ internal class ContextualExpirationCache( */ fun ContextualCache.expireAfter(duration: Duration, scope: CoroutineScope, clock: Clock): ContextualCache = ContextualExpirationCache(this, duration, clock, scope) - -@Suppress("DeprecatedCallableAddReplaceWith") -@Deprecated(message = "Specifying the clock explicitly will become mandatory in 2.0.") -fun ContextualCache.expireAfter(duration: Duration, scope: CoroutineScope): ContextualCache = - ContextualExpirationCache(this, duration, Clock.System, scope) -- 2.51.2 From 43ec7450992ac9494d1adfce295ae6b288bb537d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Fri, 20 Dec 2024 15:34:26 +0100 Subject: [PATCH 6/8] breaking(state): Remove deprecated methods See #141 --- state/src/commonMain/kotlin/outcome/Builder.kt | 3 --- state/src/commonMain/kotlin/progressive/Builder.kt | 12 ------------ 2 files changed, 15 deletions(-) diff --git a/state/src/commonMain/kotlin/outcome/Builder.kt b/state/src/commonMain/kotlin/outcome/Builder.kt index c13a55a..5bc437b 100644 --- a/state/src/commonMain/kotlin/outcome/Builder.kt +++ b/state/src/commonMain/kotlin/outcome/Builder.kt @@ -1,8 +1,5 @@ package opensavvy.state.outcome -@Deprecated("The 'success' builder has been renamed to 'successful'", replaceWith = ReplaceWith("successful()", "opensavvy.state.outcome.successful")) -fun Value.success() = Outcome.Success(this) - fun Value.successful() = Outcome.Success(this) fun Failure.failed() = Outcome.Failure(this) diff --git a/state/src/commonMain/kotlin/progressive/Builder.kt b/state/src/commonMain/kotlin/progressive/Builder.kt index f8c1cfb..c75b994 100644 --- a/state/src/commonMain/kotlin/progressive/Builder.kt +++ b/state/src/commonMain/kotlin/progressive/Builder.kt @@ -27,18 +27,6 @@ fun ProgressiveOutcome.copy(progress: Progress. is ProgressiveOutcome.Success -> ProgressiveOutcome.Success(value, progress) } -/** - * Convenience function to instantiate a [ProgressiveOutcome.Success]. - */ -@Deprecated("The 'success' factory has been replaced by 'successfulWithProgress'.", replaceWith = ReplaceWith("successfulWithProgress(progress)", "opensavvy.state.progressive.successfulWithProgress")) -fun Value.success(progress: Progress = done()) = ProgressiveOutcome.Success(this, progress) - -/** - * Convenience function to instantiate a [ProgressiveOutcome.Failure]. - */ -@Deprecated("The 'failed' factory has been replaced by 'failedWithProgress'.", replaceWith = ReplaceWith("failedWithProgress(progress)", "opensavvy.state.progressive.failedWithProgress")) -fun Failure.failed(progress: Progress = done()) = ProgressiveOutcome.Failure(this, progress) - /** * Convenience function to instantiate a [ProgressiveOutcome.Success]. */ -- 2.51.2 From 1d74adfe2d99f044040b17ee70c96b437b9efd48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Fri, 20 Dec 2024 15:36:03 +0100 Subject: [PATCH 7/8] refactor(cache): Remove the unused TestClock --- cache/src/commonTest/kotlin/TestClock.kt | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 cache/src/commonTest/kotlin/TestClock.kt diff --git a/cache/src/commonTest/kotlin/TestClock.kt b/cache/src/commonTest/kotlin/TestClock.kt deleted file mode 100644 index a75a217..0000000 --- a/cache/src/commonTest/kotlin/TestClock.kt +++ /dev/null @@ -1,16 +0,0 @@ -package opensavvy.cache - -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestCoroutineScheduler -import kotlinx.coroutines.test.TestScope -import kotlinx.datetime.Clock -import kotlinx.datetime.Instant - -@OptIn(ExperimentalCoroutinesApi::class) -class TestClock(private val scheduler: TestCoroutineScheduler) : Clock { - override fun now(): Instant = - Instant.fromEpochMilliseconds(scheduler.currentTime) -} - -@OptIn(ExperimentalCoroutinesApi::class) -val TestScope.testClock get() = TestClock(testScheduler) -- 2.51.2 From abf94fb564069a54efab3e298d55fe741cda3fa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Fri, 20 Dec 2024 15:39:06 +0100 Subject: [PATCH 8/8] style: Various code style improvements & removal of unused opt-ins --- backbone/build.gradle.kts | 24 ++++------ cache-blocking/build.gradle.kts | 24 ++++------ .../templates/template-lib/build.gradle.kts | 3 -- logger/build.gradle.kts | 19 ++------ .../kotlin/CoroutineProgressReporterTest.kt | 40 ++++++++-------- .../kotlin/ProgressReportTransformerTest.kt | 46 +++++++++---------- .../kotlin/StateFlowProgressReporterTest.kt | 40 ++++++++-------- weak/src/jsMain/kotlin/WeakMap.js.kt | 4 +- 8 files changed, 83 insertions(+), 117 deletions(-) diff --git a/backbone/build.gradle.kts b/backbone/build.gradle.kts index c04e234..8f15945 100644 --- a/backbone/build.gradle.kts +++ b/backbone/build.gradle.kts @@ -1,5 +1,3 @@ -@file:Suppress("UNUSED_VARIABLE") - plugins { alias(opensavvyConventions.plugins.base) alias(opensavvyConventions.plugins.kotlin.library) @@ -16,22 +14,16 @@ kotlin { iosX64() linuxX64() - sourceSets { - val commonMain by getting { - dependencies { - api(projects.state) - api(projects.cache) - api(libs.kotlinx.datetime) + sourceSets.commonMain.dependencies { + api(projects.state) + api(projects.cache) + api(libs.kotlinx.datetime) - implementation(projects.logger) - } - } + implementation(projects.logger) + } - val commonTest by getting { - dependencies { - implementation(libs.bundles.prepared) - } - } + sourceSets.commonTest.dependencies { + implementation(libs.bundles.prepared) } } diff --git a/cache-blocking/build.gradle.kts b/cache-blocking/build.gradle.kts index 9b2155f..33a3a5a 100644 --- a/cache-blocking/build.gradle.kts +++ b/cache-blocking/build.gradle.kts @@ -1,5 +1,3 @@ -@file:Suppress("UNUSED_VARIABLE") - plugins { alias(opensavvyConventions.plugins.base) alias(opensavvyConventions.plugins.kotlin.library) @@ -8,22 +6,16 @@ plugins { kotlin { jvm() - sourceSets { - val commonMain by getting { - dependencies { - api(projects.cache) + sourceSets.commonMain.dependencies { + api(projects.cache) - implementation(projects.logger) - } - } + implementation(projects.logger) + } - val commonTest by getting { - dependencies { - implementation(libs.bundles.prepared) - implementation(libs.lincheck) - implementation(opensavvyConventions.aligned.kotlin.test) // Needed by Lincheck - } - } + sourceSets.commonTest.dependencies { + implementation(libs.bundles.prepared) + implementation(libs.lincheck) + implementation(opensavvyConventions.aligned.kotlin.test) // Needed by Lincheck } } diff --git a/gradle/templates/template-lib/build.gradle.kts b/gradle/templates/template-lib/build.gradle.kts index 067e592..8d17752 100644 --- a/gradle/templates/template-lib/build.gradle.kts +++ b/gradle/templates/template-lib/build.gradle.kts @@ -1,11 +1,8 @@ -import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl - plugins { alias(opensavvyConventions.plugins.base) alias(opensavvyConventions.plugins.kotlin.library) } -@OptIn(ExperimentalWasmDsl::class) kotlin { jvm() js { diff --git a/logger/build.gradle.kts b/logger/build.gradle.kts index f8b9029..50404b5 100644 --- a/logger/build.gradle.kts +++ b/logger/build.gradle.kts @@ -1,5 +1,3 @@ -@file:Suppress("UNUSED_VARIABLE") - import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask plugins { @@ -18,19 +16,12 @@ kotlin { iosX64() linuxX64() - sourceSets { - val commonMain by getting - val commonTest by getting { - dependencies { - implementation(libs.bundles.prepared) - } - } + sourceSets.jvmMain.dependencies { + implementation(libs.slf4j) + } - val jvmMain by getting { - dependencies { - implementation(libs.slf4j) - } - } + sourceSets.commonTest.dependencies { + implementation(libs.bundles.prepared) } } diff --git a/progress-coroutines/src/commonTest/kotlin/CoroutineProgressReporterTest.kt b/progress-coroutines/src/commonTest/kotlin/CoroutineProgressReporterTest.kt index 32826dd..7c30cfc 100644 --- a/progress-coroutines/src/commonTest/kotlin/CoroutineProgressReporterTest.kt +++ b/progress-coroutines/src/commonTest/kotlin/CoroutineProgressReporterTest.kt @@ -1,40 +1,38 @@ package opensavvy.progress.coroutines -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.withContext import opensavvy.prepared.runner.kotest.PreparedSpec import opensavvy.progress.Progress import opensavvy.progress.loading import opensavvy.progress.report.ProgressReporter -@OptIn(ExperimentalCoroutinesApi::class) class CoroutineProgressReporterTest : PreparedSpec({ - test("Report a value through the coroutine context") { - var value: Progress? = null + test("Report a value through the coroutine context") { + var value: Progress? = null - val reporter = ProgressReporter { value = it } - .asCoroutineContext() + val reporter = ProgressReporter { value = it } + .asCoroutineContext() - withContext(reporter) { - report(loading(0.2)) - } + withContext(reporter) { + report(loading(0.2)) + } - check(value == loading(0.2)) - } + check(value == loading(0.2)) + } - test("Report without a reporter should do nothing") { - report(loading(0.2)) // does nothing, doesn't crash - } + test("Report without a reporter should do nothing") { + report(loading(0.2)) // does nothing, doesn't crash + } - test("Report a value using a callback") { - var value: Progress? = null + test("Report a value using a callback") { + var value: Progress? = null - reportProgress({ value = it }) { - report(loading(0.2)) - } + reportProgress({ value = it }) { + report(loading(0.2)) + } - check(value == loading(0.2)) - } + check(value == loading(0.2)) + } }) diff --git a/progress-coroutines/src/commonTest/kotlin/ProgressReportTransformerTest.kt b/progress-coroutines/src/commonTest/kotlin/ProgressReportTransformerTest.kt index 6f1aed9..ab8930b 100644 --- a/progress-coroutines/src/commonTest/kotlin/ProgressReportTransformerTest.kt +++ b/progress-coroutines/src/commonTest/kotlin/ProgressReportTransformerTest.kt @@ -1,42 +1,40 @@ package opensavvy.progress.coroutines -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.withContext import opensavvy.prepared.runner.kotest.PreparedSpec import opensavvy.progress.Progress import opensavvy.progress.loading import opensavvy.progress.report.ProgressReporter -@OptIn(ExperimentalCoroutinesApi::class) class ProgressReportTransformerTest : PreparedSpec({ - test("Using the progress transformer") { - var value: Progress? = null + test("Using the progress transformer") { + var value: Progress? = null - withContext(ProgressReporter { value = it }.asCoroutineContext()) { - report(loading(0.1)) - check(value == loading(0.1)) + withContext(ProgressReporter { value = it }.asCoroutineContext()) { + report(loading(0.1)) + check(value == loading(0.1)) - mapProgressTo(0.2..0.5) { - report(loading(0.0)) - check(value == loading(0.2)) + mapProgressTo(0.2..0.5) { + report(loading(0.0)) + check(value == loading(0.2)) - report(loading(0.5)) - check(value == loading(0.35)) + report(loading(0.5)) + check(value == loading(0.35)) - report(loading(1.0)) - check(value == loading(0.5)) - } + report(loading(1.0)) + check(value == loading(0.5)) + } - report(loading(0.9)) - check(value == loading(0.9)) - } + report(loading(0.9)) + check(value == loading(0.9)) + } - // Calling the function without reporter should no-op, NOT fail - mapProgressTo(0.2..0.5) { - report(loading(0.0)) - check(value == loading(0.9)) // The value shouldn't be impacted, since we should no-op - } - } + // Calling the function without reporter should no-op, NOT fail + mapProgressTo(0.2..0.5) { + report(loading(0.0)) + check(value == loading(0.9)) // The value shouldn't be impacted, since we should no-op + } + } }) diff --git a/progress-coroutines/src/commonTest/kotlin/StateFlowProgressReporterTest.kt b/progress-coroutines/src/commonTest/kotlin/StateFlowProgressReporterTest.kt index 6e42cf3..85d4384 100644 --- a/progress-coroutines/src/commonTest/kotlin/StateFlowProgressReporterTest.kt +++ b/progress-coroutines/src/commonTest/kotlin/StateFlowProgressReporterTest.kt @@ -1,6 +1,5 @@ package opensavvy.progress.coroutines -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.transformWhile import kotlinx.coroutines.yield @@ -9,33 +8,32 @@ import opensavvy.prepared.suite.launch import opensavvy.progress.done import opensavvy.progress.loading -@OptIn(ExperimentalCoroutinesApi::class) class StateFlowProgressReporterTest : PreparedSpec({ - test("Reporting progress events through a StateFlow") { - val reporter = StateFlowProgressReporter() + test("Reporting progress events through a StateFlow") { + val reporter = StateFlowProgressReporter() - launch { - reporter.report(loading(0.2)) - yield() + launch { + reporter.report(loading(0.2)) + yield() - reporter.report(loading(0.3)) - yield() + reporter.report(loading(0.3)) + yield() - check(reporter.toString() == "StateFlowProgressReporter(progress=Loading(30%))") + check(reporter.toString() == "StateFlowProgressReporter(progress=Loading(30%))") - reporter.report(done()) - yield() - } + reporter.report(done()) + yield() + } - val expected = listOf( - loading(0.0), - loading(0.2), - loading(0.3), - done(), - ) + val expected = listOf( + loading(0.0), + loading(0.2), + loading(0.3), + done(), + ) - check(expected == reporter.progress.transformWhile { emit(it); it != done() }.toList()) - } + check(expected == reporter.progress.transformWhile { emit(it); it != done() }.toList()) + } }) diff --git a/weak/src/jsMain/kotlin/WeakMap.js.kt b/weak/src/jsMain/kotlin/WeakMap.js.kt index 89bc5ea..faa15ff 100644 --- a/weak/src/jsMain/kotlin/WeakMap.js.kt +++ b/weak/src/jsMain/kotlin/WeakMap.js.kt @@ -3,7 +3,7 @@ package opensavvy.pedestal.weak import js.array.tupleOf private class JsWeakMap( - private val wrapped: js.collections.WeakMap + private val wrapped: js.collections.WeakMap, ) : WeakMap { override fun get(key: K): V? { if (key == null) @@ -57,7 +57,7 @@ actual fun WeakMap(): WeakMap = */ @ExperimentalWeakApi actual fun WeakMap(values: Map): WeakMap { - val map = js.collections.WeakMap( + val map = js.collections.WeakMap( values .mapNotNull { (k, v) -> if (k == null) -- 2.51.2