I'm using the generate function from `UriTemplate`...
# http4k
c
I'm using the generate function from
UriTemplate
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:
s
Can you share a bit more? The way I approached this in the past was having type-safe URL schema classes (e.g.
MyCoolAppUrlScheme
) 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.
c
@s4nchez Sure. Btw i started with krouton lib, but removed it as it did not integrate well with the rest of the app. Now I have this:
Copy code
data 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().path
What I want is the
path
function on
UrlPath
to fail when not all parameters are provided. I do that now with this check
renderedPath.contains('{')