From a9c41e1d55546e59a18a3c5c459bd0dbedc58b53 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Mon, 23 Mar 2026 19:31:58 +0100 Subject: [PATCH 1/7] Introduce uri-reference-kmp and replace uriBridge usages with it. --- quartz/build.gradle.kts | 3 +++ .../vitorpamplona/quartz/utils/Rfc3986.ios.kt | 20 +++++++++---------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index 93a3214c5..3aad8ae61 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -144,6 +144,7 @@ kotlin { } binaries.framework { baseName = xcfName + //isStatic = true binaryOption("bundleId", "com.vitorpamplona.quartz") } } @@ -154,6 +155,7 @@ kotlin { } binaries.framework { baseName = xcfName + //isStatic = true binaryOption("bundleId", "com.vitorpamplona.quartz") } } @@ -329,6 +331,7 @@ kotlin { implementation(libs.dev.whyoleg.cryptography.provider.apple.optimal) implementation("io.github.andreypfau:kotlinx-crypto-hmac:0.0.4") implementation("io.github.andreypfau:kotlinx-crypto-sha2:0.0.4") + implementation("io.github.kotlingeekdev:uri-reference-kmp:1.0") } } diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/Rfc3986.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/Rfc3986.ios.kt index c782de2f9..9c7784a03 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/Rfc3986.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/Rfc3986.ios.kt @@ -20,30 +20,30 @@ */ package com.vitorpamplona.quartz.utils +import io.kotlingeekdev.urireference.URIReference import kotlinx.cinterop.ExperimentalForeignApi -import platform.Foundation.NSURLComponents -import swiftbridge.Rfc3986UriBridge @OptIn(ExperimentalForeignApi::class) actual object Rfc3986 { - private val rfc3986UriBridge = Rfc3986UriBridge() actual fun normalize(uri: String): String = - rfc3986UriBridge - .normalizeUrlWithUrl(uri, null) - ?.let { if (it.last() == '/') it else "$it/" } ?: throw Exception("Could not normalize URI: $uri") + URIReference.parse(uri).normalize().toString() - actual fun isValidUrl(url: String): Boolean = rfc3986UriBridge.isUrlValidWithUrl(url) + actual fun isValidUrl(url: String): Boolean = runCatching { + URIReference.parse(url) + }.isSuccess actual fun normalizeAndRemoveFragment(url: String): String = - NSURLComponents(url) + URIReference + .parse(url) + .normalize()!! .toStringNoFragment() .internIfPossible() - actual fun host(url: String): String = rfc3986UriBridge.hostFromUriWithUrl(url, null) ?: throw Exception("Could not retrieve host from URL.") + actual fun host(url: String): String = URIReference.parse(url).host?.value.toString() } -fun NSURLComponents.toStringNoFragment(): String { +fun URIReference.toStringNoFragment(): String { val sb = StringBuilder() if (scheme != null) sb.append(scheme).append(":") From f2bdb24ee56056e9ed353da0507f67c069fbe7c9 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Mon, 23 Mar 2026 19:38:10 +0100 Subject: [PATCH 2/7] Removal of references to APIs using the Swift bridge. --- .../com/vitorpamplona/quartz/utils/Rfc3986.ios.kt | 2 -- .../kotlin/com/vitorpamplona/quartz/utils/URLs.ios.kt | 11 +++-------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/Rfc3986.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/Rfc3986.ios.kt index 9c7784a03..69290fc35 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/Rfc3986.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/Rfc3986.ios.kt @@ -21,9 +21,7 @@ package com.vitorpamplona.quartz.utils import io.kotlingeekdev.urireference.URIReference -import kotlinx.cinterop.ExperimentalForeignApi -@OptIn(ExperimentalForeignApi::class) actual object Rfc3986 { actual fun normalize(uri: String): String = diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/URLs.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/URLs.ios.kt index ad697c7f9..46a95ee3c 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/URLs.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/URLs.ios.kt @@ -20,12 +20,7 @@ */ package com.vitorpamplona.quartz.utils -import kotlinx.cinterop.ExperimentalForeignApi -import swiftbridge.UrlDetector +import com.vitorpamplona.quartz.utils.urldetector.detection.UrlDetector -@Suppress("UNCHECKED_CAST") -@OptIn(ExperimentalForeignApi::class) -actual fun fastFindURLs(text: String): List { - val detectorInstance = UrlDetector() - return detectorInstance.findURLsWithText(text) as List -} + +actual fun fastFindURLs(text: String): List = UrlDetector(text).detect().map { it.originalUrl } From cfecfe213047b21214a8caa50b264859254dbc2a Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Mon, 23 Mar 2026 19:49:24 +0100 Subject: [PATCH 3/7] Move uri-reference-kmp to version catalog. Remove swift bridge(was a nice run). --- gradle/libs.versions.toml | 2 ++ quartz/build.gradle.kts | 22 +----------------- .../swift/swiftbridge/Rfc3986UriBridge.swift | 23 ------------------- .../src/swift/swiftbridge/UrlDetector.swift | 21 ----------------- 4 files changed, 3 insertions(+), 65 deletions(-) delete mode 100644 quartz/src/swift/swiftbridge/Rfc3986UriBridge.swift delete mode 100644 quartz/src/swift/swiftbridge/UrlDetector.swift diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1a2b35468..899dbb406 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -58,6 +58,7 @@ torAndroid = "0.4.9.5.1" translate = "17.0.3" jetbrainsCompose = "1.10.3" unifiedpush = "3.0.10" +uriReferenceKmp = "1.0" vico-charts-compose = "3.0.3" zelory = "3.0.1" zoomable = "2.11.1" @@ -128,6 +129,7 @@ coil-okhttp = { group = "io.coil-kt.coil3", name = "coil-network-okhttp", versio coil-video = { group = "io.coil-kt.coil3", name = "coil-video", version.ref = "coil" } commons-imaging = { group = "org.apache.commons", name = "commons-imaging", version.ref = "commonsImaging" } slf4j-nop = { module = "org.slf4j:slf4j-nop", version.ref = "slf4j" } +uri-reference-kmp = { module = "io.github.kotlingeekdev:uri-reference-kmp", version.ref = "uriReferenceKmp" } vlcj = { group = "uk.co.caprica", name = "vlcj", version.ref = "vlcj" } dev-whyoleg-cryptography-provider-apple-optimal = { module = "dev.whyoleg.cryptography:cryptography-provider-optimal", version.ref = "devWhyolegCryptography" } drfonfon-geohash = { group = "com.github.drfonfon", name = "android-kotlin-geohash", version.ref = "androidKotlinGeohash" } diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index 3aad8ae61..3b0eca3cf 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -1,9 +1,5 @@ -@file:OptIn(ExperimentalSpmForKmpFeature::class) - import com.vanniktech.maven.publish.KotlinMultiplatform import com.vanniktech.maven.publish.SourcesJar -import io.github.frankois944.spmForKmp.swiftPackageConfig -import io.github.frankois944.spmForKmp.utils.ExperimentalSpmForKmpFeature import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeTest @@ -13,7 +9,6 @@ plugins { alias(libs.plugins.androidKotlinMultiplatformLibrary) alias(libs.plugins.serialization) alias(libs.plugins.vanniktech.mavenPublish) - alias(libs.plugins.frankois944.spmForKmp) } kotlin { @@ -121,21 +116,6 @@ kotlin { dependsOn(libsodiumDefFileGeneration) } } - - target.swiftPackageConfig(cinteropName = "swiftbridge") { - minIos = "17" - minMacos = "14" - dependency { - remotePackageVersion( - url = uri("https://github.com/swift-standards/swift-rfc-3986.git"), - packageName = "swift-rfc-3986", - products = { - add("RFC 3986") - }, - version = "0.1.0", - ) - } - } } iosArm64 { @@ -331,7 +311,7 @@ kotlin { implementation(libs.dev.whyoleg.cryptography.provider.apple.optimal) implementation("io.github.andreypfau:kotlinx-crypto-hmac:0.0.4") implementation("io.github.andreypfau:kotlinx-crypto-sha2:0.0.4") - implementation("io.github.kotlingeekdev:uri-reference-kmp:1.0") + implementation(libs.uri.reference.kmp) } } diff --git a/quartz/src/swift/swiftbridge/Rfc3986UriBridge.swift b/quartz/src/swift/swiftbridge/Rfc3986UriBridge.swift deleted file mode 100644 index bafd34a5b..000000000 --- a/quartz/src/swift/swiftbridge/Rfc3986UriBridge.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// Created by NullDev on 31/12/2025. -// - -import Foundation -import RFC_3986 - -@objcMembers public class Rfc3986UriBridge: NSObject { - public func normalizeUrl(url: String) throws -> String { - let uri = try RFC_3986.URI(url) - let normalized = uri.normalized() - return normalized.value - } - - public func isUrlValid(url: String) -> Bool { - return RFC_3986.isValidURI(url) - } - - public func hostFromUri(url: String) throws -> String { - let actualUri = try RFC_3986.URI(url) - return actualUri.host! - } -} diff --git a/quartz/src/swift/swiftbridge/UrlDetector.swift b/quartz/src/swift/swiftbridge/UrlDetector.swift deleted file mode 100644 index e4927c516..000000000 --- a/quartz/src/swift/swiftbridge/UrlDetector.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// Created by NullDev on 20/01/2026. -// - -import Foundation - -@objcMembers public class UrlDetector: NSObject { - public func findURLs(text: String) -> [String] { - var links = [String]() - let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) - let matches = detector.matches(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count)) - - for match in matches { - guard let range = Range(match.range, in: text) else { continue } - let url = text[range] - links.append(String(url)) - } - - return links - } -} \ No newline at end of file From b495f3f409ed1c08087dc7b694802483a36c4e6d Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Mon, 23 Mar 2026 19:50:26 +0100 Subject: [PATCH 4/7] NIP19 tests for iOS. --- .../quartz/nip19Bech32/NIP19EmbedTests.kt | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip19Bech32/NIP19EmbedTests.kt diff --git a/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip19Bech32/NIP19EmbedTests.kt b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip19Bech32/NIP19EmbedTests.kt new file mode 100644 index 000000000..f5042e2d1 --- /dev/null +++ b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip19Bech32/NIP19EmbedTests.kt @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip19Bech32 + +import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair +import com.vitorpamplona.quartz.nip01Core.crypto.verify +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed +import com.vitorpamplona.quartz.utils.Hex +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class NIP19EmbedTests { + @Test + fun testEmbedKind1Event() = + runTest { + val signer = + NostrSignerInternal( + KeyPair(Hex.decode("e8e7197ccc53c9ed4cf9b1c8dce085475fa1ffdd71f2c14e44fe23d0bdf77598")), + ) + + val textNote = + signer.sign( + TextNoteEvent.build("I like this. It could solve the ninvite problem in #1062, and it seems like it could be applied very broadly to limit the spread of events that shouldn't stand on their own or need to be private. The one question I have is how long are these embeds? If it's 50 lines of text, that breaks the human readable (or at least parseable) requirement of kind 1s. Also, encoding json in a tlv is silly, we should at least use the tlv to reduce the payload size."), + ) + + assertNotNull(textNote) + + val bech32 = NEmbed.create(textNote) + + println(bech32) + + val decodedNote = (Nip19Parser.uriToRoute(bech32)?.entity as NEmbed).event + + assertTrue(decodedNote.verify()) + + assertEquals(textNote.toJson(), decodedNote.toJson()) + } + + @Test + fun testVisionPrescriptionEmbedEvent() = + runTest { + val signer = + NostrSignerInternal( + KeyPair(Hex.decode("e8e7197ccc53c9ed4cf9b1c8dce085475fa1ffdd71f2c14e44fe23d0bdf77598")), + ) + + val eyeglassesPrescriptionEvent = signer.sign(FhirResourceEvent.build(visionPrescriptionFhir)) + + assertNotNull(eyeglassesPrescriptionEvent) + + val bech32 = NEmbed.create(eyeglassesPrescriptionEvent) + + println(eyeglassesPrescriptionEvent.toJson()) + println(bech32) + + val decodedNote = (Nip19Parser.uriToRoute(bech32)?.entity as NEmbed).event + + assertTrue(decodedNote.verify()) + + assertEquals(eyeglassesPrescriptionEvent.toJson(), decodedNote.toJson()) + } + + @Test + fun testVisionPrescriptionBundleEmbedEvent() = + runTest { + val signer = + NostrSignerInternal( + KeyPair(Hex.decode("e8e7197ccc53c9ed4cf9b1c8dce085475fa1ffdd71f2c14e44fe23d0bdf77598")), + ) + + val eyeglassesPrescriptionEvent = signer.sign(FhirResourceEvent.build(visionPrescriptionBundle)) + + assertNotNull(eyeglassesPrescriptionEvent) + + val bech32 = NEmbed.create(eyeglassesPrescriptionEvent) + + println(eyeglassesPrescriptionEvent.toJson()) + println(bech32) + + val decodedNote = (Nip19Parser.uriToRoute(bech32)?.entity as NEmbed).event + + assertTrue(decodedNote.verify()) + + assertEquals(eyeglassesPrescriptionEvent.toJson(), decodedNote.toJson()) + } + + @Test + fun testVisionPrescriptionBundle2EmbedEvent() = + runTest { + val signer = + NostrSignerInternal( + KeyPair(decodePrivateKeyAsHexOrNull("nsec1arn3jlxv20y76n8ek8ydecy9ga06rl7aw8evznjylc3ap00hwkvqx4vvy6")!!.hexToByteArray()), + ) + + val eyeglassesPrescriptionEvent = signer.sign(FhirResourceEvent.build(visionPrescriptionBundle2)) + + assertNotNull(eyeglassesPrescriptionEvent) + + val bech32 = NEmbed.create(eyeglassesPrescriptionEvent) + + println(eyeglassesPrescriptionEvent.toJson()) + println(bech32) + + val decodedNote = (Nip19Parser.uriToRoute(bech32)?.entity as NEmbed).event + + assertTrue(decodedNote.verify()) + + assertEquals(eyeglassesPrescriptionEvent.toJson(), decodedNote.toJson()) + } + + @Test + fun testTimsNembed() { + val uri = "nembed1r79ssq9446hkwqhl642ukmku8qg0c92pu7w3j0jyfte8tc7tvg85vmrys8x3sqgle5vjy7jpjswqhphl0kd6yf4sz0n3peyjq5rp3zkat4w6c6j3f7um0724jmfu5456xxgg2yxkn8dp23j64xsn9npcggzafyh2effyntqrqxzja8dp52kpcvc9zqxlj86e8mx05vevzxkeprjkfs4wmppxm3p96vj6yvu2mqgf5l4v99492r2qsggquxuv93uzx244652h2kkj8xseg9xkq0afpygknjtty9j4ju5v0nm9mezux9wyl6s5wr7lzce7cj397mnu0u04ha7aq3w7exelrhe3zs3l3urwa9sp36u80npllrs0hmsxqdn0fsuyav3nv0azjs5suzuurg2uymncjxez8p9xksc2j6gw992enjflgrdd7n5uq2xrpvfrd3rckw624ey0elvm6grr27tyzlf4vaswgm5vc3hdyczsl983g2j8e67r6z5zt30lat84ma4wclkwwxxrcflvdsuwd7346h7zqav4vdwe3gkt9lr87sfk4aqd2aey03tt4eyspldrqcmkx9pqe2pn63rv7grwwalr86akuldnvjm6m87wrw9sdwns8wq0rnsmj57vqwtc3g7hkwum3vl2dda78dwkycgfzw6qna3ufhpatcvq5a4hm4ehl45an8umwt0clf7rn77ctke475qglwu86hhfwhn7dkca4pkfpyc4y75rll6nvr5qc8nlhf8mk22celn5mecvyuzxd830drhdck9tcdpcafymk8wajwu2w8ha8gatggjfvq0a4jlf2sdamzj0ysqks9dk8me3q7a0qpmf6vykurkrcls4pug3u4pn4u26ezx3h8e482n07x2nsmu80dpufxqc0ttcyzhnppguxma4d8aumdawnlsyy7yzcuxl7lw5y9p4nv5h8fn6u8anpm2tsze3p6mgxy9j9uuqfxg2jvlmtjpakna5m4hln0msmw804hnun96h66fh62270yhhljnmmdl7jln07ll5vft7e870hemcld34a09n943ed6629fgtctsftma9q6tf4jfm2p0ukd2j2n2dpz53fqrkk4ctdcy2j5jar095g5jntf6u807ggkzauzt6uqkwk4tg5w7w55kskspc9663zx5dzzzfwpg3q546g2ve4kukr70n0a46eyce2crsqqq247ql5" + + val decodedNote = (Nip19Parser.uriToRoute(uri)?.entity as NEmbed).event + + assertTrue(decodedNote.verify()) + + assertEquals(timsPrescription, decodedNote.toJson()) + } + + val visionPrescriptionFhir = "{\"resourceType\":\"VisionPrescription\",\"status\":\"active\",\"created\":\"2014-06-15\",\"patient\":{\"reference\":\"Patient/Donald Duck\"},\"dateWritten\":\"2014-06-15\",\"prescriber\":{\"reference\":\"Practitioner/Adam Careful\"},\"lensSpecification\":[{\"eye\":\"right\",\"sphere\":-2,\"prism\":[{\"amount\":0.5,\"base\":\"down\"}],\"add\":2},{\"eye\":\"left\",\"sphere\":-1,\"cylinder\":-0.5,\"axis\":180,\"prism\":[{\"amount\":0.5,\"base\":\"up\"}],\"add\":2}]}" + val visionPrescriptionBundle = "{\"resourceType\":\"Bundle\",\"id\":\"bundle-vision-test\",\"type\":\"document\",\"entry\":[{\"resourceType\":\"Practitioner\",\"id\":\"2\",\"active\":true,\"name\":[{\"use\":\"official\",\"family\":\"Careful\",\"given\":[\"Adam\"]}],\"gender\":\"male\"},{\"resourceType\":\"Patient\",\"id\":\"1\",\"active\":true,\"name\":[{\"use\":\"official\",\"family\":\"Duck\",\"given\":[\"Donald\"]}],\"gender\":\"male\"},{\"resourceType\":\"VisionPrescription\",\"status\":\"active\",\"created\":\"2014-06-15\",\"patient\":{\"reference\":\"#1\"},\"dateWritten\":\"2014-06-15\",\"prescriber\":{\"reference\":\"#2\"},\"lensSpecification\":[{\"eye\":\"right\",\"sphere\":-2,\"prism\":[{\"amount\":0.5,\"base\":\"down\"}],\"add\":2},{\"eye\":\"left\",\"sphere\":-1,\"cylinder\":-0.5,\"axis\":180,\"prism\":[{\"amount\":0.5,\"base\":\"up\"}],\"add\":2}]}]}" + + val visionPrescriptionBundle2 = "{\"resourceType\":\"Bundle\",\"id\":\"bundle-vision-test\",\"type\":\"document\",\"entry\":[{\"resourceType\":\"Practitioner\",\"id\":\"2\",\"active\":true,\"name\":[{\"use\":\"official\",\"family\":\"Smith\",\"given\":[\"Dr. Joe\"]}],\"gender\":\"male\"},{\"resourceType\":\"Patient\",\"id\":\"1\",\"active\":true,\"name\":[{\"use\":\"official\",\"family\":\"Doe\",\"given\":[\"Jane\"]}],\"gender\":\"male\"},{\"resourceType\":\"VisionPrescription\",\"status\":\"active\",\"created\":\"2014-06-15\",\"patient\":{\"reference\":\"#1\"},\"dateWritten\":\"2014-06-15\",\"lensSpecification\":[{\"eye\":\"right\",\"sphere\":-2,\"prism\":[{\"amount\":0.5,\"base\":\"down\"}],\"add\":2},{\"eye\":\"left\",\"sphere\":-1,\"cylinder\":-0.5,\"axis\":180,\"prism\":[{\"amount\":0.5,\"base\":\"up\"}],\"add\":2}]}]}" + val timsPrescription = "{\"id\":\"18d8b22e6455dfc9f4c6d6be8c2cf015e961b8d160dfe5e4b7fc1578f2c4e0be\",\"pubkey\":\"46f1826abf5b03de972192e619e25fa94d775a1c555efe53a775412dbf49889b\",\"created_at\":1739566773,\"kind\":82,\"tags\":[[\"p\",\"46f1826abf5b03de972192e619e25fa94d775a1c555efe53a775412dbf49889b\"]],\"content\":\"{\\\"resourceType\\\": \\\"VisionPrescription\\\", \\\"id\\\": \\\"eyeglass-prescription-001\\\", \\\"status\\\": \\\"active\\\", \\\"created\\\": \\\"2025-02-14T10:00:00Z\\\", \\\"patient\\\": {\\\"reference\\\": \\\"Patient/12345\\\", \\\"display\\\": \\\"John Doe\\\"}, \\\"encounter\\\": {\\\"reference\\\": \\\"Encounter/67890\\\"}, \\\"dateWritten\\\": \\\"2025-02-10T15:00:00Z\\\", \\\"prescriber\\\": {\\\"reference\\\": \\\"Practitioner/56789\\\", \\\"display\\\": \\\"Dr. Emily Smith\\\"}, \\\"lensSpecification\\\": [{\\\"product\\\": {\\\"coding\\\": [{\\\"system\\\": \\\"http://terminology.hl7.org/CodeSystem/ex-visionprescriptionproduct\\\", \\\"code\\\": \\\"lens\\\", \\\"display\\\": \\\"Eyeglasses\\\"}]}, \\\"eye\\\": \\\"right\\\", \\\"sphere\\\": -2.5, \\\"cylinder\\\": -1.0, \\\"axis\\\": 180, \\\"prism\\\": [{\\\"amount\\\": 0.5, \\\"base\\\": \\\"up\\\"}], \\\"add\\\": 2.0, \\\"duration\\\": {\\\"value\\\": 24, \\\"unit\\\": \\\"months\\\", \\\"system\\\": \\\"http://unitsofmeasure.org\\\", \\\"code\\\": \\\"mo\\\"}, \\\"note\\\": [{\\\"text\\\": \\\"Right eye prescription for near-sightedness with astigmatism.\\\"}]}, {\\\"product\\\": {\\\"coding\\\": [{\\\"system\\\": \\\"http://terminology.hl7.org/CodeSystem/ex-visionprescriptionproduct\\\", \\\"code\\\": \\\"lens\\\", \\\"display\\\": \\\"Eyeglasses\\\"}]}, \\\"eye\\\": \\\"left\\\", \\\"sphere\\\": -3.0, \\\"cylinder\\\": -0.75, \\\"axis\\\": 160, \\\"prism\\\": [{\\\"amount\\\": 0.5, \\\"base\\\": \\\"down\\\"}], \\\"add\\\": 2.0, \\\"duration\\\": {\\\"value\\\": 24, \\\"unit\\\": \\\"months\\\", \\\"system\\\": \\\"http://unitsofmeasure.org\\\", \\\"code\\\": \\\"mo\\\"}, \\\"note\\\": [{\\\"text\\\": \\\"Left eye prescription for near-sightedness with astigmatism.\\\"}]}]}\",\"sig\":\"d22d3b86aea397094de8b6cdf69decdfd886c90008aeebf95fd43a2770b37d486b313bff5bdb44b78e33bbaa3f336d74ee8b36bc5b16050374054246c72d93c2\"}" +} From bddff36e6ba134aafaeaeef47c1eea28815d768c Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Mon, 23 Mar 2026 19:55:35 +0100 Subject: [PATCH 5/7] androidLibrary {} -> android {} --- quartz/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index 3b0eca3cf..30d120c07 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -21,7 +21,7 @@ kotlin { } } - androidLibrary { + android { namespace = "com.vitorpamplona.quartz" compileSdk = libs.versions.android.compileSdk From ac38a73b5eaf5e1e2ec3831e8bb4a30dd605c77e Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Mon, 23 Mar 2026 20:27:04 +0100 Subject: [PATCH 6/7] spotless apply. --- .../vitorpamplona/quartz/utils/Rfc3986.ios.kt | 18 +++++++++++------- .../com/vitorpamplona/quartz/utils/URLs.ios.kt | 1 - 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/Rfc3986.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/Rfc3986.ios.kt index 69290fc35..07da65b6d 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/Rfc3986.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/Rfc3986.ios.kt @@ -23,13 +23,12 @@ package com.vitorpamplona.quartz.utils import io.kotlingeekdev.urireference.URIReference actual object Rfc3986 { + actual fun normalize(uri: String): String = URIReference.parse(uri).normalize().toString() - actual fun normalize(uri: String): String = - URIReference.parse(uri).normalize().toString() - - actual fun isValidUrl(url: String): Boolean = runCatching { - URIReference.parse(url) - }.isSuccess + actual fun isValidUrl(url: String): Boolean = + runCatching { + URIReference.parse(url) + }.isSuccess actual fun normalizeAndRemoveFragment(url: String): String = URIReference @@ -38,7 +37,12 @@ actual object Rfc3986 { .toStringNoFragment() .internIfPossible() - actual fun host(url: String): String = URIReference.parse(url).host?.value.toString() + actual fun host(url: String): String = + URIReference + .parse(url) + .host + ?.value + .toString() } fun URIReference.toStringNoFragment(): String { diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/URLs.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/URLs.ios.kt index 46a95ee3c..bdb01fd24 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/URLs.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/URLs.ios.kt @@ -22,5 +22,4 @@ package com.vitorpamplona.quartz.utils import com.vitorpamplona.quartz.utils.urldetector.detection.UrlDetector - actual fun fastFindURLs(text: String): List = UrlDetector(text).detect().map { it.originalUrl } From 349781d9beea06051e89384415b1781809c2027d Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Tue, 24 Mar 2026 16:34:13 +0100 Subject: [PATCH 7/7] Use uri-reference-kmp in commonMain. Remove platform-specific implementations. --- quartz/build.gradle.kts | 11 ++-- .../com/vitorpamplona/quartz/utils/Rfc3986.kt | 45 +++++++++++-- .../vitorpamplona/quartz/utils/Rfc3986.ios.kt | 57 ---------------- .../quartz/utils/Rfc3986.jvmAndroid.kt | 65 ------------------- 4 files changed, 45 insertions(+), 133 deletions(-) delete mode 100644 quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/Rfc3986.ios.kt delete mode 100644 quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/Rfc3986.jvmAndroid.kt diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index b9a251f69..2e79a5e2c 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -124,7 +124,7 @@ kotlin { } binaries.framework { baseName = xcfName - //isStatic = true + isStatic = true binaryOption("bundleId", "com.vitorpamplona.quartz") } } @@ -135,7 +135,7 @@ kotlin { } binaries.framework { baseName = xcfName - //isStatic = true + isStatic = true binaryOption("bundleId", "com.vitorpamplona.quartz") } } @@ -183,6 +183,9 @@ kotlin { // SQLite KMP driver for event store api(libs.androidx.sqlite) implementation(libs.androidx.sqlite.bundled) + + // RFC3986 library(normalizes URLs) + api(libs.uri.reference.kmp) } } @@ -203,9 +206,6 @@ kotlin { dependsOn(commonMain.get()) dependencies { - // Normalizes URLs - api(libs.rfc3986.normalizer) - // Performant Parser of JSONs into Events api(libs.jackson.module.kotlin) @@ -311,7 +311,6 @@ kotlin { implementation(libs.dev.whyoleg.cryptography.provider.apple.optimal) implementation("io.github.andreypfau:kotlinx-crypto-hmac:0.0.4") implementation("io.github.andreypfau:kotlinx-crypto-sha2:0.0.4") - implementation(libs.uri.reference.kmp) } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Rfc3986.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Rfc3986.kt index 3503c2edf..a0bc76d7f 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Rfc3986.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Rfc3986.kt @@ -20,12 +20,47 @@ */ package com.vitorpamplona.quartz.utils -expect object Rfc3986 { - fun normalize(uri: String): String +import io.kotlingeekdev.urireference.URIReference - fun isValidUrl(url: String): Boolean +object Rfc3986 { + fun normalize(uri: String): String = URIReference.parse(uri).normalize().toString() - fun normalizeAndRemoveFragment(url: String): String + fun isValidUrl(url: String): Boolean = + runCatching { + URIReference.parse(url) + }.isSuccess - fun host(url: String): String + fun normalizeAndRemoveFragment(url: String): String = + URIReference + .parse(url) + .normalize()!! + .toStringNoFragment() + .internIfPossible() + + fun host(url: String): String = + URIReference + .parse(url) + .host + ?.value + .toString() +} + +fun URIReference.toStringSchemeHost(): String { + val sb = StringBuilder() + + if (scheme != null) sb.append(scheme).append(":") + if (authority != null) sb.append("//").append(authority.toString()) + + return sb.toString() +} + +fun URIReference.toStringNoFragment(): String { + val sb = StringBuilder() + + if (scheme != null) sb.append(scheme).append(":") + if (host != null) sb.append("//").append(host.toString()) + if (path != null) sb.append(path) + if (query != null) sb.append("?").append(query) + + return sb.toString() } diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/Rfc3986.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/Rfc3986.ios.kt deleted file mode 100644 index 07da65b6d..000000000 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/Rfc3986.ios.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.utils - -import io.kotlingeekdev.urireference.URIReference - -actual object Rfc3986 { - actual fun normalize(uri: String): String = URIReference.parse(uri).normalize().toString() - - actual fun isValidUrl(url: String): Boolean = - runCatching { - URIReference.parse(url) - }.isSuccess - - actual fun normalizeAndRemoveFragment(url: String): String = - URIReference - .parse(url) - .normalize()!! - .toStringNoFragment() - .internIfPossible() - - actual fun host(url: String): String = - URIReference - .parse(url) - .host - ?.value - .toString() -} - -fun URIReference.toStringNoFragment(): String { - val sb = StringBuilder() - - if (scheme != null) sb.append(scheme).append(":") - if (host != null) sb.append("//").append(host.toString()) - if (path != null) sb.append(path) - if (query != null) sb.append("?").append(query) - - return sb.toString() -} diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/Rfc3986.jvmAndroid.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/Rfc3986.jvmAndroid.kt deleted file mode 100644 index 459567892..000000000 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/Rfc3986.jvmAndroid.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.utils - -import org.czeal.rfc3986.URIReference - -actual object Rfc3986 { - actual fun normalize(uri: String) = - URIReference - .parse(uri) - .normalize() - .toString() - - actual fun isValidUrl(url: String): Boolean = - runCatching { - URIReference.parse(url) - }.isSuccess - - actual fun normalizeAndRemoveFragment(url: String): String = - URIReference - .parse(url) - .normalize() - .toStringNoFragment() - .intern() - - actual fun host(url: String): String = URIReference.parse(url).host.value -} - -fun URIReference.toStringSchemeHost(): String { - val sb = StringBuilder() - - if (scheme != null) sb.append(scheme).append(":") - if (authority != null) sb.append("//").append(authority.toString()) - - return sb.toString() -} - -fun URIReference.toStringNoFragment(): String { - val sb = StringBuilder() - - if (scheme != null) sb.append(scheme).append(":") - if (authority != null) sb.append("//").append(authority.toString()) - if (path != null) sb.append(path) - if (query != null) sb.append("?").append(query) - - return sb.toString() -}