Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ conventions are wired into the build and are authoritative:
(synchronized pins carrier threads under Loom). Blocking calls must respect
`Thread.interrupt()` — catch `InterruptedException`, restore the interrupt flag, and
throw `InterruptedIOException` (or attach the interrupt as suppressed).
- **Commit style:** `feat:` / `test:` / `docs:` / `chore:` prefixes (`merge:` for merge
commits). **PR titles follow the same prefixed style** (e.g. `docs: add CLAUDE.md`).
- **Commit style:** `feat:` / `test:` / `docs:` / `chore:` / `refactor:` prefixes (`merge:`
for merge commits). **PR titles follow the same prefixed style** (e.g. `docs: add CLAUDE.md`).

## Generated data & codegen

Expand Down
42 changes: 0 additions & 42 deletions kuri/src/commonMain/kotlin/org/dexpace/kuri/ParseProfile.kt

This file was deleted.

19 changes: 10 additions & 9 deletions kuri/src/commonMain/kotlin/org/dexpace/kuri/Uri.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import org.dexpace.kuri.error.UriSyntaxException
import org.dexpace.kuri.error.map
import org.dexpace.kuri.host.Host
import org.dexpace.kuri.parser.BuilderPath
import org.dexpace.kuri.parser.ComponentPath
import org.dexpace.kuri.parser.ParsedComponents
import org.dexpace.kuri.parser.Resolver
import org.dexpace.kuri.parser.UriParser
import org.dexpace.kuri.parser.UrlPath
import org.dexpace.kuri.parser.decodedSegments
import org.dexpace.kuri.parser.fileExtensionOf
import org.dexpace.kuri.parser.fileNameOf
Expand All @@ -25,8 +25,8 @@ import org.dexpace.kuri.query.QueryParametersBuilder
import org.dexpace.kuri.query.QueryState
import org.dexpace.kuri.query.applyParameterEdit
import org.dexpace.kuri.scheme.Scheme
import org.dexpace.kuri.serialize.Serializer
import org.dexpace.kuri.serialize.UriNormalizer
import org.dexpace.kuri.serialize.UriSerializer
import org.dexpace.kuri.serialize.guardRecomposedUriPath
import kotlin.jvm.JvmName
import kotlin.jvm.JvmOverloads
Expand Down Expand Up @@ -187,7 +187,7 @@ public class Uri internal constructor(
public fun fileExtension(): String = fileExtensionOf(fileName())

/** Cached canonical-but-unnormalized serialization, computed once. */
private val canonicalUri: String by lazy { Serializer.serialize(components, ParseProfile.URI) }
private val canonicalUri: String by lazy { UriSerializer.serialize(components) }

/**
* The canonical-but-UNNORMALIZED RFC 3986 §5.3 serialization; the basis of equality.
Expand Down Expand Up @@ -444,7 +444,7 @@ public class Uri internal constructor(
*
* @return `true` iff this URI is absolute with an authority-less, rootless (opaque) path.
*/
public fun isOpaquePath(): Boolean = components.path is UrlPath.Opaque || hasRootlessSchemePath()
public fun isOpaquePath(): Boolean = components.path is ComponentPath.Opaque || hasRootlessSchemePath()

/**
* The port a consumer should connect to: the explicit [port], else this scheme's default.
Expand Down Expand Up @@ -534,13 +534,14 @@ public class Uri internal constructor(
* Reuses the already-decoded [decodedPathSegments] rather than decoding every segment a second
* time: an opaque path is that single decoded value, and a segment path rejoins the decoded
* segments through [toUriPathString] so the empty-vs-root-only and rooted-vs-rootless ordering
* keeps its single source of truth (UrlPath.kt). Reading [decodedPathSegments] here is safe —
* keeps its single source of truth (ComponentPath.kt). Reading [decodedPathSegments] here is safe —
* it does not read [decodedPath], so there is no lazy cycle.
*/
private fun computeDecodedPath(): String =
when (val storedPath = components.path) {
is UrlPath.Opaque -> decodedPathSegments.single()
is UrlPath.Segments -> UrlPath.Segments(decodedPathSegments, storedPath.rooted).toUriPathString()
is ComponentPath.Opaque -> decodedPathSegments.single()
is ComponentPath.Segments ->
ComponentPath.Segments(decodedPathSegments, storedPath.rooted).toUriPathString()
}

/** The decoded segments backing [pathSegments]; an opaque path yields its single decoded value. */
Expand All @@ -550,15 +551,15 @@ public class Uri internal constructor(
/**
* True for the RFC 3986 opaque shape: an absolute URI with no authority and a rootless path.
*
* Reads the structured [UrlPath.Segments.rooted] flag rather than re-serializing the path and
* Reads the structured [ComponentPath.Segments.rooted] flag rather than re-serializing the path and
* inspecting its first character. A rootless path serializes with a leading `/` only when its first
* segment is empty, so a non-empty first segment is the same condition as `!startsWith("/")` —
* without the `O(path)` string build on every [isOpaquePath]/[relativize] call.
*/
private fun hasRootlessSchemePath(): Boolean {
if (scheme == null || components.host != null) return false
val storedPath = components.path
return storedPath is UrlPath.Segments &&
return storedPath is ComponentPath.Segments &&
!storedPath.rooted &&
storedPath.segments.firstOrNull()?.isNotEmpty() == true
}
Expand Down
14 changes: 7 additions & 7 deletions kuri/src/commonMain/kotlin/org/dexpace/kuri/Url.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import org.dexpace.kuri.error.ValidationError
import org.dexpace.kuri.error.map
import org.dexpace.kuri.host.Host
import org.dexpace.kuri.parser.BuilderPath
import org.dexpace.kuri.parser.ComponentPath
import org.dexpace.kuri.parser.ParsedComponents
import org.dexpace.kuri.parser.StateOverride
import org.dexpace.kuri.parser.UrlParser
import org.dexpace.kuri.parser.UrlPath
import org.dexpace.kuri.parser.decodedSegments
import org.dexpace.kuri.parser.fileExtensionOf
import org.dexpace.kuri.parser.fileNameOf
Expand All @@ -25,7 +25,7 @@ import org.dexpace.kuri.query.QueryParametersBuilder
import org.dexpace.kuri.query.QueryState
import org.dexpace.kuri.query.applyParameterEdit
import org.dexpace.kuri.scheme.Scheme
import org.dexpace.kuri.serialize.Serializer
import org.dexpace.kuri.serialize.UrlSerializer
import org.dexpace.kuri.serialize.serializeAuthority
import org.dexpace.kuri.serialize.serializeUrlPath
import kotlin.jvm.JvmName
Expand Down Expand Up @@ -214,7 +214,7 @@ public class Url internal constructor(
public fun hasOpaqueOrigin(): Boolean = origin == OPAQUE_ORIGIN

/** Cached canonical serialization, computed once (permits caching an immutable value). */
private val canonicalHref: String by lazy { Serializer.serialize(components, ParseProfile.URL) }
private val canonicalHref: String by lazy { UrlSerializer.serialize(components) }

/** Path/query projections, each computed once; every value is immutable, mirroring [canonicalHref]. */
private val decodedPathSegments: List<String> by lazy {
Expand Down Expand Up @@ -356,7 +356,7 @@ public class Url internal constructor(
*
* @return the last non-empty decoded segment, or `""` when there is none or the path is opaque.
*/
public fun fileName(): String = if (components.path is UrlPath.Opaque) "" else fileNameOf(pathSegments)
public fun fileName(): String = if (components.path is ComponentPath.Opaque) "" else fileNameOf(pathSegments)

/**
* The extension of [fileName]: the text after its last `.`, or `""` when it has none (SPEC §3.3).
Expand Down Expand Up @@ -416,7 +416,7 @@ public class Url internal constructor(
* @return the updated [Url], or `this` when the setter is a WHATWG no-op.
*/
public fun withPathname(value: String): Url {
if (components.path is UrlPath.Opaque) return this
if (components.path is ComponentPath.Opaque) return this
return applyOverride(value, StateOverride.PATHNAME)
}

Expand Down Expand Up @@ -527,7 +527,7 @@ public class Url internal constructor(
* @return the updated [Url], or `this` when the setter is a WHATWG no-op.
*/
public fun withHost(value: String): Url {
if (components.path is UrlPath.Opaque) return this
if (components.path is ComponentPath.Opaque) return this
return applyOverride(value, StateOverride.HOST)
}

Expand All @@ -540,7 +540,7 @@ public class Url internal constructor(
* @return the updated [Url], or `this` when the setter is a WHATWG no-op.
*/
public fun withHostname(value: String): Url {
if (components.path is UrlPath.Opaque) return this
if (components.path is ComponentPath.Opaque) return this
return applyOverride(value, StateOverride.HOSTNAME)
}

Expand Down
37 changes: 37 additions & 0 deletions kuri/src/commonMain/kotlin/org/dexpace/kuri/host/HostParsing.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2026 dexpace and Omar Aljarrah
* SPDX-License-Identifier: MIT
*/
package org.dexpace.kuri.host

import org.dexpace.kuri.error.HostError
import org.dexpace.kuri.error.ParseResult
import org.dexpace.kuri.error.UriParseError

/**
* Opening delimiter of an IP-literal; a host beginning with it dispatches to the IPv6/IPvFuture
* grammars. Shared by [UriHostParser] and [UrlHostParser] ([HOST-4]/[HOST-5]).
*/
internal const val BRACKET_OPEN: Char = '['

/** Closing delimiter an IP-literal MUST carry ([HOST-4]/[HOST-5]); shared by both host parsers. */
internal const val BRACKET_CLOSE: Char = ']'

/**
* Strips the surrounding `[`/`]`, returning the bracket contents ([HOST-4]/[HOST-5]).
*
* Shared by both the WHATWG ([UrlHostParser]) and RFC 3986 ([UriHostParser]) bracketed-literal
* paths, which both isolate the interior before delegating to the IPv6/IPvFuture grammars.
*/
internal fun bracketInterior(input: String): String {
require(input.length >= 2) { "bracketed literal too short: $input" }
return input.substring(1, input.length - 1)
}

/**
* The malformed-literal failure for a bracketed host that does not close ([HOST-4]/[HOST-5]).
*
* Shared by both host parsers so an unterminated `[`…` reports the same [HostError.Ipv6Malformed].
*/
internal fun bracketError(input: String): ParseResult<Host> =
ParseResult.Err(UriParseError.InvalidHost(input, HostError.Ipv6Malformed))
Loading
Loading