diff --git a/.gitignore b/.gitignore
index 13a066e..2a8f5da 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,85 +1,72 @@
-!.vscode/extensions.json
-!.vscode/launch.json
-!.vscode/settings.json
-!.vscode/tasks.json
-*.class
-*.code-workspace
-*.ctxt
-*.iws
-*.log
-*.nar
-*.rar
-*.sublime-*
-*.tar.gz
-*.zip
-.DS_Store
-.classpath
+### Gradle ###
.gradle
-.history
-.kobalt
-.mtj.tmp/
-.mvn/timing.properties
-.mvn/wrapper/maven-wrapper.jar
-.nb-gradle
-.project
-.scannerwork
-.settings
-.vscode/*
-/**/.idea/$CACHE_FILE$
-/**/.idea/$PRODUCT_WORKSPACE_FILE$
-/**/.idea/**/caches/build_file_checksums.ser
-/**/.idea/**/contentModel.xml
-/**/.idea/**/dataSources.ids
-/**/.idea/**/dataSources.local.xml
-/**/.idea/**/dataSources/
-/**/.idea/**/dbnavigator.xml
-/**/.idea/**/dictionaries
-/**/.idea/**/dynamic.xml
-/**/.idea/**/gradle.xml
-/**/.idea/**/httpRequests
-/**/.idea/**/libraries
-/**/.idea/**/mongoSettings.xml
-/**/.idea/**/replstate.xml
-/**/.idea/**/shelf
-/**/.idea/**/shelf/
-/**/.idea/**/sqlDataSources.xml
-/**/.idea/**/tasks.xml
-/**/.idea/**/uiDesigner.xml
-/**/.idea/**/usage.statistics.xml
-/**/.idea/**/workspace.xml
-/**/.idea/sonarlint*
-/**/.idea_modules/
-Thumbs.db
-__pycache__
-atlassian-ide-plugin.xml
-bin/
build/
-cmake-build-*/
-com_crashlytics_export_strings.xml
-crashlytics-build.properties
-crashlytics.properties
-dependency-reduced-pom.xml
deploy/
-dist/
-ehthumbs.db
-fabric.properties
-gen/
-gradle.properties
+
+### Kotlin/JVM ###
+*.class
+*.log
+
hs_err_pid*
-kobaltBuild
-kobaltw*-test
-lib/kotlin*
-libs/
-local.properties
-out/
-pom.xml.asc
-pom.xml.next
-pom.xml.releaseBackup
-pom.xml.tag
-pom.xml.versionsBackup
-proguard-project.txt
-project.properties
-release.properties
-target/
-test-output
-venv
+replay_pid*
+*.hprof
+
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+
+### IntelliJ ###
+.idea/**/*
+
+### Eclipse ###
+.metadata
+bin/
+tmp/
+*.tmp
+*.bak
+*.swp
+*~.nib
+.settings/
+.loadpath
+.recommenders
+.classpath
+
+.apt_generated/
+.apt_generated_test/
+.project
+
+
+### Linux ###
+*~
+.fuse_hidden*
+.Trash-*
+.nfs*
+
+
+### Windows ###
+[Dd]esktop.ini
+$RECYCLE.BIN/
+*.lnk
+
+
+### macOS ###
+.DS_Store
+._*
+
+# Icon must end with two \r
+Icon
+
+
+###########################
+
+# place overrides last, so they're not themselves overridden
+
+!gradle/wrapper/gradle-wrapper.jar
+!gradle/wrapper/gradle-wrapper.properties
+
+!.idea/copyright/**
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 26d3352..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
deleted file mode 100644
index fb7f4a8..0000000
--- a/.idea/compiler.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
deleted file mode 100644
index 1e01b48..0000000
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
deleted file mode 100644
index cfd3ca8..0000000
--- a/.idea/jarRepositories.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
deleted file mode 100644
index 2b8a50f..0000000
--- a/.idea/kotlinc.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index 2540c68..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 35eb1dd..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..3dd9d03
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2001-2023 Geert Bevin (gbevin[remove] at uwyn dot com)
+ * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ buildsrc.conventions.base
+}
+
+group = "net.thauvin.erik"
+version = "1.3.1-SNAPSHOT"
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
new file mode 100644
index 0000000..788b119
--- /dev/null
+++ b/buildSrc/build.gradle.kts
@@ -0,0 +1,12 @@
+plugins {
+ `kotlin-dsl`
+}
+
+dependencies {
+ implementation("com.github.ben-manes:gradle-versions-plugin:0.44.0")
+ implementation("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.22.0")
+ implementation("org.jetbrains.dokka:dokka-gradle-plugin:1.7.20")
+ implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0")
+ implementation("org.jetbrains.kotlinx:kover-gradle-plugin:0.7.0")
+ implementation("org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.5.0.2730")
+}
diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts
new file mode 100644
index 0000000..bf6ac65
--- /dev/null
+++ b/buildSrc/settings.gradle.kts
@@ -0,0 +1,16 @@
+rootProject.name = "buildSrc"
+
+pluginManagement {
+ repositories {
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+@Suppress("UnstableApiUsage")
+dependencyResolutionManagement {
+ repositories {
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
diff --git a/buildSrc/src/main/kotlin/buildsrc/conventions/base.gradle.kts b/buildSrc/src/main/kotlin/buildsrc/conventions/base.gradle.kts
new file mode 100644
index 0000000..59e3a56
--- /dev/null
+++ b/buildSrc/src/main/kotlin/buildsrc/conventions/base.gradle.kts
@@ -0,0 +1,18 @@
+package buildsrc.conventions
+
+/** common config for all subprojects */
+
+plugins {
+ base
+}
+
+if (project != rootProject) {
+ project.version = rootProject.version
+ project.group = rootProject.group
+}
+
+tasks.withType().configureEach {
+ // https://docs.gradle.org/current/userguide/working_with_files.html#sec:reproducible_archives
+ isPreserveFileTimestamps = false
+ isReproducibleFileOrder = true
+}
diff --git a/buildSrc/src/main/kotlin/buildsrc/conventions/code-quality.gradle.kts b/buildSrc/src/main/kotlin/buildsrc/conventions/code-quality.gradle.kts
new file mode 100644
index 0000000..f9c3d13
--- /dev/null
+++ b/buildSrc/src/main/kotlin/buildsrc/conventions/code-quality.gradle.kts
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2001-2023 Geert Bevin (gbevin[remove] at uwyn dot com)
+ * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package buildsrc.conventions
+
+import buildsrc.utils.Rife2TestListener
+import org.gradle.api.tasks.testing.logging.TestExceptionFormat
+import org.gradle.api.tasks.testing.logging.TestLogEvent
+import org.sonarqube.gradle.SonarTask
+
+plugins {
+ id("org.sonarqube")
+ id("io.gitlab.arturbosch.detekt")
+ id("org.jetbrains.kotlinx.kover")
+}
+
+sonarqube {
+ properties {
+ property("sonar.projectName", rootProject.name)
+ property("sonar.projectKey", "ethauvin_${rootProject.name}")
+ property("sonar.organization", "ethauvin-github")
+ property("sonar.host.url", "https://sonarcloud.io")
+ property("sonar.sourceEncoding", "UTF-8")
+ property("sonar.coverage.jacoco.xmlReportPaths", "${project.buildDir}/reports/kover/report.xml")
+ }
+}
+
+tasks.withType().configureEach {
+ dependsOn(tasks.matching { it.name == "koverXmlReport" })
+}
+
+tasks.withType().configureEach {
+ val testsBadgeApiKey = providers.gradleProperty("testsBadgeApiKey")
+ addTestListener(Rife2TestListener(testsBadgeApiKey))
+ testLogging {
+ exceptionFormat = TestExceptionFormat.FULL
+ events = setOf(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
+ }
+}
diff --git a/buildSrc/src/main/kotlin/buildsrc/conventions/lang/kotlin-jvm.gradle.kts b/buildSrc/src/main/kotlin/buildsrc/conventions/lang/kotlin-jvm.gradle.kts
new file mode 100644
index 0000000..790e694
--- /dev/null
+++ b/buildSrc/src/main/kotlin/buildsrc/conventions/lang/kotlin-jvm.gradle.kts
@@ -0,0 +1,42 @@
+package buildsrc.conventions.lang
+
+import buildsrc.utils.Rife2TestListener
+import org.gradle.api.JavaVersion
+import org.gradle.api.tasks.testing.Test
+import org.gradle.api.tasks.testing.logging.TestExceptionFormat
+import org.gradle.api.tasks.testing.logging.TestLogEvent
+import org.gradle.kotlin.dsl.withType
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+import org.sonarqube.gradle.SonarTask
+
+/**
+ * Common configuration for Kotlin/JVM projects
+ *
+ * (this can be removed after Kotlin Multiplatform migration)
+ */
+
+plugins {
+ id("buildsrc.conventions.base")
+ kotlin("jvm")
+ id("buildsrc.conventions.code-quality")
+}
+
+java {
+ withSourcesJar()
+}
+
+kotlin {
+ jvmToolchain(11)
+}
+
+tasks.withType().configureEach {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_11)
+ }
+}
+
+tasks.withType().configureEach {
+ useJUnitPlatform()
+}
diff --git a/buildSrc/src/main/kotlin/buildsrc/conventions/lang/kotlin-multiplatform-base.gradle.kts b/buildSrc/src/main/kotlin/buildsrc/conventions/lang/kotlin-multiplatform-base.gradle.kts
new file mode 100644
index 0000000..4af978b
--- /dev/null
+++ b/buildSrc/src/main/kotlin/buildsrc/conventions/lang/kotlin-multiplatform-base.gradle.kts
@@ -0,0 +1,45 @@
+package buildsrc.conventions.lang
+
+import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget
+
+
+/**
+ * Base configuration for all Kotlin/Multiplatform conventions.
+ *
+ * This plugin does not enable any Kotlin target. To enable a target in a subproject, prefer applying specific Kotlin
+ * target convention plugins.
+ */
+
+plugins {
+ id("buildsrc.conventions.base")
+ kotlin("multiplatform")
+}
+
+
+kotlin {
+ jvmToolchain(11)
+
+ targets.configureEach {
+ compilations.configureEach {
+ kotlinOptions {
+ // nothin' yet
+ }
+ }
+ }
+
+ // configure all Kotlin/JVM Tests to use JUnit
+ targets.withType().configureEach {
+ testRuns.configureEach {
+ executionTask.configure {
+ useJUnitPlatform()
+ }
+ }
+ }
+
+ sourceSets.configureEach {
+ languageSettings {
+// languageVersion =
+// apiVersion =
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/buildsrc/conventions/lang/kotlin-multiplatform-js.gradle.kts b/buildSrc/src/main/kotlin/buildsrc/conventions/lang/kotlin-multiplatform-js.gradle.kts
new file mode 100644
index 0000000..2a0034a
--- /dev/null
+++ b/buildSrc/src/main/kotlin/buildsrc/conventions/lang/kotlin-multiplatform-js.gradle.kts
@@ -0,0 +1,18 @@
+package buildsrc.conventions.lang
+
+/** conventions for a Kotlin/JS subproject */
+
+plugins {
+ id("buildsrc.conventions.lang.kotlin-multiplatform-base")
+}
+
+kotlin {
+ targets {
+ js(IR) {
+ browser()
+ nodejs()
+ }
+ }
+}
+
+relocateKotlinJsStore()
diff --git a/buildSrc/src/main/kotlin/buildsrc/conventions/lang/kotlin-multiplatform-jvm.gradle.kts b/buildSrc/src/main/kotlin/buildsrc/conventions/lang/kotlin-multiplatform-jvm.gradle.kts
new file mode 100644
index 0000000..6bbef24
--- /dev/null
+++ b/buildSrc/src/main/kotlin/buildsrc/conventions/lang/kotlin-multiplatform-jvm.gradle.kts
@@ -0,0 +1,11 @@
+package buildsrc.conventions.lang
+
+plugins {
+ id("buildsrc.conventions.lang.kotlin-multiplatform-base")
+}
+
+kotlin {
+ jvm {
+ withJava()
+ }
+}
diff --git a/buildSrc/src/main/kotlin/buildsrc/conventions/lang/kotlin-multiplatform-native.gradle.kts b/buildSrc/src/main/kotlin/buildsrc/conventions/lang/kotlin-multiplatform-native.gradle.kts
new file mode 100644
index 0000000..7e8bb2a
--- /dev/null
+++ b/buildSrc/src/main/kotlin/buildsrc/conventions/lang/kotlin-multiplatform-native.gradle.kts
@@ -0,0 +1,102 @@
+package buildsrc.conventions.lang
+
+/** conventions for a Kotlin/Native subproject */
+
+plugins {
+ id("buildsrc.conventions.lang.kotlin-multiplatform-base")
+}
+
+kotlin {
+
+ // Native targets all extend commonMain and commonTest.
+ //
+ // Some targets (ios, tvos, watchos) are shortcuts provided by the Kotlin DSL, that
+ // provide additional targets, except for 'simulators' which must be defined manually.
+ // https://kotlinlang.org/docs/multiplatform-share-on-platforms.html#use-target-shortcuts
+ //
+ // common/
+ // └── native/
+ // ├── linuxX64
+ // ├── mingwX64
+ // ├── macosX64
+ // ├── macosArm64
+ // ├── ios/ (shortcut)
+ // │ ├── iosArm64
+ // │ ├── iosX64
+ // │ └── iosSimulatorArm64
+ // ├── tvos/ (shortcut)
+ // │ ├── tvosArm64
+ // │ ├── tvosX64
+ // │ └── tvosSimulatorArm64Main
+ // └── watchos/ (shortcut)
+ // ├── watchosArm32
+ // ├── watchosArm64
+ // ├── watchosX64
+ // └── watchosSimulatorArm64Main
+
+ targets {
+ linuxX64()
+
+ mingwX64()
+
+ macosX64()
+ macosArm64()
+
+ // https://kotlinlang.org/docs/multiplatform-share-on-platforms.html#use-target-shortcuts
+ ios() // iosArm64, iosX64
+ watchos() // watchosArm32, watchosArm64, watchosX64
+ tvos() // tvosArm64, tvosX64
+
+ iosSimulatorArm64()
+ tvosSimulatorArm64()
+ watchosSimulatorArm64()
+ }
+
+ @Suppress("UNUSED_VARIABLE")
+ sourceSets {
+ val commonMain by getting {}
+ val commonTest by getting {}
+
+ val nativeMain by creating { dependsOn(commonMain) }
+ val nativeTest by creating { dependsOn(commonTest) }
+
+ // Linux
+ val linuxX64Main by getting { dependsOn(nativeMain) }
+ val linuxX64Test by getting { dependsOn(nativeTest) }
+
+ // Windows - MinGW
+ val mingwX64Main by getting { dependsOn(nativeMain) }
+ val mingwX64Test by getting { dependsOn(nativeTest) }
+
+ // Apple - macOS
+ val macosArm64Main by getting { dependsOn(nativeMain) }
+ val macosArm64Test by getting { dependsOn(nativeTest) }
+
+ val macosX64Main by getting { dependsOn(nativeMain) }
+ val macosX64Test by getting { dependsOn(nativeTest) }
+
+ // Apple - iOS
+ val iosMain by getting { dependsOn(nativeMain) }
+ val iosTest by getting { dependsOn(nativeTest) }
+
+// val iosSimulatorArm64Main by getting { dependsOn(iosMain) }
+// val iosSimulatorArm64Test by getting { dependsOn(iosTest) }
+
+// // Apple - tvOS
+// val tvosMain by getting { dependsOn(nativeMain) }
+// val tvosTest by getting { dependsOn(nativeTest) }
+//
+// val tvosSimulatorArm64Main by getting { dependsOn(tvosMain) }
+// val tvosSimulatorArm64Test by getting { dependsOn(tvosTest) }
+//
+// // Apple - watchOS
+// val watchosMain by getting { dependsOn(nativeMain) }
+// val watchosTest by getting { dependsOn(nativeTest) }
+//
+// val watchosSimulatorArm64Main by getting { dependsOn(watchosMain) }
+// val watchosSimulatorArm64Test by getting { dependsOn(watchosTest) }
+
+ // val iosArm32Main by getting { dependsOn(desktopMain) }
+ // val iosArm32Test by getting { dependsOn(nativeTest) }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/buildsrc/conventions/lang/kotlinJsExtensions.kt b/buildSrc/src/main/kotlin/buildsrc/conventions/lang/kotlinJsExtensions.kt
new file mode 100644
index 0000000..eefdc94
--- /dev/null
+++ b/buildSrc/src/main/kotlin/buildsrc/conventions/lang/kotlinJsExtensions.kt
@@ -0,0 +1,18 @@
+package buildsrc.conventions.lang
+
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.configure
+import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension
+
+/**
+ * `kotlin-js` and `kotlin-multiplatform` plugins adds a directory in the root-dir for the Yarn
+ * lockfile. That's a bit annoying. It's a little neater if it's in the Gradle dir, next to the
+ * version catalog.
+ */
+internal fun Project.relocateKotlinJsStore() {
+ afterEvaluate {
+ rootProject.extensions.configure {
+ lockFileDirectory = project.rootDir.resolve("gradle/kotlin-js-store")
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/buildsrc/conventions/publishing.gradle.kts b/buildSrc/src/main/kotlin/buildsrc/conventions/publishing.gradle.kts
new file mode 100644
index 0000000..2390479
--- /dev/null
+++ b/buildSrc/src/main/kotlin/buildsrc/conventions/publishing.gradle.kts
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2001-2023 Geert Bevin (gbevin[remove] at uwyn dot com)
+ * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package buildsrc.conventions
+
+plugins {
+ id("maven-publish")
+ id("signing")
+ id("org.jetbrains.dokka")
+}
+
+val gitHub = "ethauvin/${rootProject.name}"
+val mavenUrl = "https://github.com/$gitHub"
+val isSnapshotVersion = { project.version.toString().contains("SNAPSHOT") }
+
+publishing {
+ publications {
+ withType().configureEach {
+ pom {
+ name.set("UrlEncoder for Kotlin")
+ description.set(project.description)
+ url.set(mavenUrl)
+ licenses {
+ license {
+ name.set("The Apache License, Version 2.0")
+ url.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
+ }
+ }
+ developers {
+ developer {
+ id.set("gbevin")
+ name.set("Geert Bevin")
+ email.set("gbevin@uwyn.com")
+ url.set("https://github.com/gbevin")
+ }
+ developer {
+ id.set("ethauvin")
+ name.set("Erik C. Thauvin")
+ email.set("erik@thauvin.net")
+ url.set("https://erik.thauvin.net/")
+ }
+ }
+ scm {
+ connection.set("scm:git://github.com/$gitHub.git")
+ developerConnection.set("scm:git@github.com:$gitHub.git")
+ url.set(mavenUrl)
+ }
+ issueManagement {
+ system.set("GitHub")
+ url.set("$mavenUrl/issues")
+ }
+ }
+ }
+ }
+ repositories {
+ maven(
+ if (isSnapshotVersion()) {
+ uri("https://oss.sonatype.org/content/repositories/snapshots/")
+ } else {
+ uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
+ }
+ ) {
+ name = "ossrh"
+ credentials(PasswordCredentials::class)
+ }
+ }
+}
+
+signing {
+ useGpgCmd()
+ sign(publishing.publications)
+
+ setRequired({
+ !isSnapshotVersion() || gradle.taskGraph.hasTask("publish")
+ })
+}
+tasks.withType().configureEach {
+ val signingRequiredPredicate = provider { signing.isRequired }
+ onlyIf { signingRequiredPredicate.get() }
+}
+
+// https://youtrack.jetbrains.com/issue/KT-46466
+val signingTasks = tasks.withType()
+tasks.withType().configureEach {
+ dependsOn(signingTasks)
+}
+
+val javadocJar by tasks.registering(Jar::class) {
+ dependsOn(tasks.dokkaJavadoc)
+ from(tasks.dokkaJavadoc)
+ archiveClassifier.set("javadoc")
+}
diff --git a/buildSrc/src/main/kotlin/buildsrc/utils/Rife2TestListener.kt b/buildSrc/src/main/kotlin/buildsrc/utils/Rife2TestListener.kt
new file mode 100644
index 0000000..748b064
--- /dev/null
+++ b/buildSrc/src/main/kotlin/buildsrc/utils/Rife2TestListener.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2001-2023 Geert Bevin (gbevin[remove] at uwyn dot com)
+ * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package buildsrc.utils
+
+import org.gradle.api.provider.Provider
+import org.gradle.api.tasks.testing.TestDescriptor
+import org.gradle.api.tasks.testing.TestListener
+import org.gradle.api.tasks.testing.TestResult
+import java.net.URI
+import java.net.http.HttpClient
+import java.net.http.HttpRequest
+import java.net.http.HttpResponse
+
+class Rife2TestListener(
+ private val testBadgeApiKey: Provider
+) : TestListener {
+ override fun beforeTest(p0: TestDescriptor?) = Unit
+ override fun beforeSuite(p0: TestDescriptor?) = Unit
+ override fun afterTest(desc: TestDescriptor, result: TestResult) = Unit
+ override fun afterSuite(desc: TestDescriptor, result: TestResult) {
+ if (desc.parent == null) {
+ val passed = result.successfulTestCount
+ val failed = result.failedTestCount
+ val skipped = result.skippedTestCount
+
+ val apiKey = testBadgeApiKey.orNull
+
+ if (apiKey != null) {
+ println(apiKey)
+ val response: HttpResponse = HttpClient.newHttpClient()
+ .send(
+ HttpRequest.newBuilder()
+ .uri(
+ URI(
+ "https://rife2.com/tests-badge/update/net.thauvin.erik/urlencoder?" +
+ "apiKey=$apiKey&" +
+ "passed=$passed&" +
+ "failed=$failed&" +
+ "skipped=$skipped"
+ )
+ )
+ .POST(HttpRequest.BodyPublishers.noBody())
+ .build(), HttpResponse.BodyHandlers.ofString()
+ )
+ println("RESPONSE: ${response.statusCode()}")
+ println(response.body())
+ }
+ }
+ }
+}
diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts
index 5b12abf..eb0edae 100644
--- a/lib/build.gradle.kts
+++ b/lib/build.gradle.kts
@@ -1,42 +1,17 @@
-import org.gradle.api.tasks.testing.logging.TestExceptionFormat
-import org.gradle.api.tasks.testing.logging.TestLogEvent
import org.jetbrains.dokka.gradle.DokkaTask
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
-import java.net.URI
-import java.net.http.HttpClient
-import java.net.http.HttpRequest
-import java.net.http.HttpResponse
plugins {
+ buildsrc.conventions.lang.`kotlin-jvm`
+ buildsrc.conventions.publishing
id("application")
- id("com.github.ben-manes.versions") version "0.44.0"
- id("io.gitlab.arturbosch.detekt") version "1.22.0"
- id("java-library")
- id("maven-publish")
- id("org.jetbrains.dokka") version "1.7.20"
- id("org.jetbrains.kotlin.jvm") version "1.8.0"
- id("org.jetbrains.kotlinx.kover") version "0.6.1"
- id("org.sonarqube") version "3.5.0.2730"
- id("signing")
+ id("com.github.ben-manes.versions")
}
description = "A simple defensive library to encode/decode URL components"
-group = "net.thauvin.erik"
-version = "1.3.1-SNAPSHOT"
-
-val mavenName = "UrlEncoder"
-val deployDir = "deploy"
-val gitHub = "ethauvin/${rootProject.name}"
-val mavenUrl = "https://github.com/$gitHub"
-val publicationName = "mavenJava"
-val myClassName = "$group.${rootProject.name}.$mavenName"
-
-repositories {
- mavenCentral()
- maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") }
-}
+val deployDir = project.layout.projectDirectory.dir("deploy")
+val urlEncoderMainClass = "net.thauvin.erik.urlencoder.UrlEncoder"
dependencies {
// testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.25")
@@ -47,42 +22,19 @@ base {
archivesName.set(rootProject.name)
}
-java {
- sourceCompatibility = JavaVersion.VERSION_11
- targetCompatibility = JavaVersion.VERSION_11
- withSourcesJar()
-}
-
application {
- mainClass.set(myClassName)
-}
-
-sonarqube {
- properties {
- property("sonar.projectName", rootProject.name)
- property("sonar.projectKey", "ethauvin_${rootProject.name}")
- property("sonar.organization", "ethauvin-github")
- property("sonar.host.url", "https://sonarcloud.io")
- property("sonar.sourceEncoding", "UTF-8")
- property("sonar.coverage.jacoco.xmlReportPaths", "${project.buildDir}/reports/kover/xml/report.xml")
- }
-}
-
-val javadocJar by tasks.creating(Jar::class) {
- dependsOn(tasks.dokkaJavadoc)
- from(tasks.dokkaJavadoc)
- archiveClassifier.set("javadoc")
+ mainClass.set(urlEncoderMainClass)
}
tasks {
jar {
manifest {
- attributes["Main-Class"] = myClassName
+ attributes["Main-Class"] = urlEncoderMainClass
}
}
- val fatJar = register("fatJar") {
- group = "build"
+ val fatJar by registering(Jar::class) {
+ group = LifecycleBasePlugin.BUILD_GROUP
dependsOn.addAll(listOf("compileJava", "compileKotlin", "processResources"))
archiveClassifier.set("all")
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
@@ -97,65 +49,10 @@ tasks {
dependsOn(fatJar)
}
- withType().configureEach {
- kotlinOptions.jvmTarget = java.targetCompatibility.toString()
- }
-
- test {
- useJUnitPlatform()
- addTestListener(object : TestListener {
- override fun beforeTest(p0: TestDescriptor?) = Unit
- override fun beforeSuite(p0: TestDescriptor?) = Unit
- override fun afterTest(desc: TestDescriptor, result: TestResult) = Unit
- override fun afterSuite(desc: TestDescriptor, result: TestResult) {
- if (desc.parent == null) {
- val passed = result.successfulTestCount
- val failed = result.failedTestCount
- val skipped = result.skippedTestCount
-
- if (project.properties["testsBadgeApiKey"] != null) {
- val apiKey = project.properties["testsBadgeApiKey"]
- println(apiKey)
- val response: HttpResponse = HttpClient.newHttpClient()
- .send(
- HttpRequest.newBuilder()
- .uri(
- URI(
- "https://rife2.com/tests-badge/update/net.thauvin.erik/urlencoder?" +
- "apiKey=$apiKey&" +
- "passed=$passed&" +
- "failed=$failed&" +
- "skipped=$skipped"
- )
- )
- .POST(HttpRequest.BodyPublishers.noBody())
- .build(), HttpResponse.BodyHandlers.ofString()
- )
- println("RESPONSE: ${response.statusCode()}")
- println(response.body())
- }
- }
- }
- })
- }
-
- withType {
- testLogging {
- exceptionFormat = TestExceptionFormat.FULL
- events = setOf(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
- }
- }
-
- withType {
+ withType().configureEach {
destination = file("$projectDir/pom.xml")
}
- clean {
- doLast {
- project.delete(fileTree(deployDir))
- }
- }
-
withType().configureEach {
dokkaSourceSets {
named("main") {
@@ -164,7 +61,9 @@ tasks {
}
}
- val copyToDeploy by registering(Copy::class) {
+ val copyToDeploy by registering(Sync::class) {
+ description = "Copies all needed files to the 'deploy' directory."
+ group = PublishingPlugin.PUBLISH_TASK_GROUP
from(configurations.runtimeClasspath) {
exclude("annotations-*.jar")
}
@@ -173,73 +72,22 @@ tasks {
}
register("deploy") {
- description = "Copies all needed files to the $deployDir directory."
+ description = "Copies all needed files to the 'deploy' directory."
group = PublishingPlugin.PUBLISH_TASK_GROUP
- dependsOn(clean, build, jar)
- outputs.dir(deployDir)
- inputs.files(copyToDeploy)
- mustRunAfter(clean)
+ dependsOn(build, copyToDeploy)
}
- "sonar" {
- dependsOn(koverReport)
+ clean {
+ delete(deployDir)
}
}
publishing {
publications {
- create(publicationName) {
+ create("mavenJava") {
from(components["java"])
artifactId = rootProject.name
- artifact(javadocJar)
- pom {
- name.set("$mavenName for Kotlin")
- description.set(project.description)
- url.set(mavenUrl)
- licenses {
- license {
- name.set("The Apache License, Version 2.0")
- url.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
- }
- }
- developers {
- developer {
- id.set("gbevin")
- name.set("Geert Bevin")
- email.set("gbevin@uwyn.com")
- url.set("https://github.com/gbevin")
- }
- developer {
- id.set("ethauvin")
- name.set("Erik C. Thauvin")
- email.set("erik@thauvin.net")
- url.set("https://erik.thauvin.net/")
- }
- }
- scm {
- connection.set("scm:git://github.com/$gitHub.git")
- developerConnection.set("scm:git@github.com:$gitHub.git")
- url.set(mavenUrl)
- }
- issueManagement {
- system.set("GitHub")
- url.set("$mavenUrl/issues")
- }
- }
- }
- }
- repositories {
- maven {
- name = "ossrh"
- url = if (project.version.toString().contains("SNAPSHOT"))
- uri("https://oss.sonatype.org/content/repositories/snapshots/") else
- uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
- credentials(PasswordCredentials::class)
+ artifact(tasks.javadocJar)
}
}
}
-
-signing {
- useGpgCmd()
- sign(publishing.publications[publicationName])
-}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 0cb10ee..e75db1d 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,12 +1,25 @@
-/*
- * This file was generated by the Gradle 'init' task.
- *
- * The settings file is used to specify which projects to include in your build.
- *
- * Detailed information about configuring a multi-project build in Gradle can be found
- * in the user manual at https://docs.gradle.org/7.6/userguide/multi_project_builds.html
- * This project uses @Incubating APIs which are subject to change.
- */
-
rootProject.name = "urlencoder"
-include("lib")
+
+pluginManagement {
+ repositories {
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+@Suppress("UnstableApiUsage")
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
+
+ repositories {
+ mavenCentral()
+ maven("https://oss.sonatype.org/content/repositories/snapshots") {
+ name = "Sonatype Snapshots"
+ mavenContent { snapshotsOnly() }
+ }
+ }
+}
+
+include(
+ ":lib",
+)