Cies Breijs
08/08/2025, 10:29 AMUriTemplate to build paths in a slightly more typesafe way.
When looking at it's implementation I kind of wish for there to be a version of this function that throws an error if not all parameters are filled. Like generateOrThrow . This may help in some cases where you forget to pass a parameter (and I cannot forsee any situation where you do not want a parameter filled in).
Am I on to something, or is this is stupid idea?
Here the original function:s4nchez
08/08/2025, 10:44 AMMyCoolAppUrlScheme) with methods like findItemPrice(shopId: ShopId, itemId: ItemId): Uri which would ensure paths would be built correctly.
Internally, it'd hold an uriTemplate and call generate, but it'd be impossible to create a path without the correct parameters outside that class.Cies Breijs
08/08/2025, 11:21 PMdata object Paths {
// ...
val adminRetailerList =
UrlPath(adminPortalRoot, "/retailers")
val adminRetailerView =
UrlPath(adminPortalRoot, "/retailers/{retailerId:[0-9]+}")
// ...
// RENDER FUNCTIONS
// Only needed for paths that have path parameters.
// When a path has no path parameters you can render using the `invoke` operator on [UrlPath].
fun adminRetailerView(retailerId: Long) =
verifyAdminInviteWithToken.path(mapOf("retailerId" to retailerId))
}
/**
* These represent our end-points (only the path bit not the http verb), our app's paths.
*
* http4k's [UriTemplate] represents the path template string. A [basePath] may be added at construction.
* With the [path] function it renders to a [RenderedPath] from which you can get either a "local path" or a "full url".
*/
data class UrlPath(private val template: UriTemplate, private val basePath: UrlPath? = null) {
/** Constructor for root-based templates. */
constructor(templateString: String) : this(UriTemplate.from(templateString)) {
require(templateString.isNotEmpty()) { "A path cannot be empty" }
}
/** Constructor for [basePath]-based templates. */
constructor(basePath: UrlPath, templateString: String) : this(UriTemplate.from(templateString), basePath) {
require(templateString.isNotEmpty()) { "A path cannot be empty" }
}
/** This shields the private [template] from modification. */
fun template(): String = template.toString()
/**
* The method to build domain-local paths, possibly by interpolating [parameters] into the template.
*
* Preferably this is not called directly (there are exception: like in menu rendering of in [Paths]).
*
* Instead, simply invoke the [UrlPath] (see the `operator fun invoke()` below) for paths without path parameters,
* or create a function in [Paths] by the same name of the path's field, that takes the path parameters as arguments.
*/
fun path(parameters: Map<String, Any> = mapOf()): String {
val segments = mutableListOf(template)
var currentBasePath = basePath
while (currentBasePath != null) {
segments.addFirst(currentBasePath.template)
currentBasePath = currentBasePath.basePath
}
val urlTemplate = UriTemplate.from(segments.joinToString("/") { it.toString() })
val renderedPath = "/" + urlTemplate.generate(parameters.mapValues { it.value.toString() })
if (renderedPath.contains('{')) throw IllegalStateException("Not all path parameters where filled in: $renderedPath")
return renderedPath
}
/**
* This makes it possible to call paths, in order to have them render as path.
*
* This should only be used for paths that do not have path parameters.
*/
operator fun invoke(): RenderedPath = RenderedPath(path())
}
data class RenderedPath(val path: String) {
/**
* Renders the internal [path] with protocol and domain derived of the [request].
*
* This is usually the easiest when we have the request object around.
*/
fun fullUrl(request: Request): String {
val baseUrl = buildString {
// Code copied from http4k's `Uri.toString()`
appendIfNotBlank(request.uri.scheme, request.uri.scheme, ":")
appendIfNotBlank(request.uri.authority, "//", request.uri.authority)
}
return this.fullUrl(baseUrl)
}
/** Takes the [baseUrl] as a [String] (sometimes this is more practical than passing the request). */
fun fullUrl(baseUrl: String): String = "$baseUrl$path"
}
// I define routes like this:
val adminPortalRouter = routes(
Paths.adminDashboard bind GET to ::dashboardGetHandler,
Paths.adminProfileEdit bind POST to ::profilePostHandler,
Paths.adminProfileEdit bind GET to ::profileGetHandler,
// ...
// And i generated routes with a bit more typesafety:
Paths.adminRetailerView(retailer.id)
// Paramless paths work like this, and do not need functions to be defined for them, because of the `operator fun invoke()` on [UrlPath]
Paths.adminDashboard().pathCies Breijs
08/08/2025, 11:23 PMpath function on UrlPath to fail when not all parameters are provided. I do that now with this check renderedPath.contains('{')