Skip to content

Commit

Permalink
Merge branch 'issue/krz-186_url_query_parameters' into 'main'
Browse files Browse the repository at this point in the history
KRZ-186 URL Query Parameters

See merge request reactivecore/kreuzberg!50
  • Loading branch information
nob13 committed Mar 15, 2024
2 parents 11f483f + ef18072 commit 24df084
Show file tree
Hide file tree
Showing 15 changed files with 272 additions and 75 deletions.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ val artefactVersion = versionTag.getOrElse(snapshotVersion)

ThisBuild / version := artefactVersion

ThisBuild / scalaVersion := "3.3.2"
ThisBuild / scalaVersion := "3.3.3"

ThisBuild / scalacOptions += "-Xcheck-macros"
ThisBuild / scalacOptions += "-feature"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
package kreuzberg.examples.showcase

import kreuzberg.*
import kreuzberg.examples.showcase.pages.{
ExtendedFormPage,
FormPage,
IndexPage,
LazyPage,
NotFoundPage,
WizzardPage,
XmlPage
}
import kreuzberg.examples.showcase.pages.{ExtendedFormPage, FormPage, IndexPage, LazyPage, NotFoundPage, WizzardPage, XmlPage}
import kreuzberg.examples.showcase.todo.{TodoList, TodoPage, TodoPageWithApi}
import kreuzberg.extras.{PathCodec, Route, RoutingTarget, SimpleRouter}
import kreuzberg.extras.{PathCodec, Route, RoutingTarget, SimpleRouter, UrlResource}
import kreuzberg.scalatags.*
import kreuzberg.scalatags.all.*

Expand All @@ -27,7 +19,7 @@ object App extends SimpleComponentBase {
LoadingIndicator,
SimpleRouter(
routes,
Route.DependentRoute[String](
Route.DependentRoute[UrlResource](
PathCodec.all,
s => NotFoundPage(s),
s => "Not Found"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package kreuzberg.examples.showcase.pages

import kreuzberg.*
import kreuzberg.extras.UrlResource
import kreuzberg.scalatags.*
import kreuzberg.scalatags.all.*

case class NotFoundPage(path: String) extends ComponentBase {
case class NotFoundPage(resource: UrlResource) extends ComponentBase {

override def assemble(using context: AssemblerContext): Assembly = {
div(
s"Path ${path} not found"
s"Path ${resource} not found"
)
}
}
11 changes: 7 additions & 4 deletions extras/js/src/main/scala/kreuzberg/extras/BrowserRouting.scala
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
package kreuzberg.extras

object BrowserRouting {
def getCurrentPath(): String = {
org.scalajs.dom.window.location.pathname
def getCurrentResource(): UrlResource = {
val pathname = org.scalajs.dom.window.location.pathname
val search = org.scalajs.dom.window.location.search
val fragment = org.scalajs.dom.window.location.hash
UrlResource(pathname + search + fragment)
}

def setDocumentTitle(title: String): Unit = {
org.scalajs.dom.document.title = title
}

def pushState(title: String, target: String): Unit = {
org.scalajs.dom.window.history.pushState((), title, target)
}

def replaceState(title: String, target: String): Unit = {
org.scalajs.dom.window.history.replaceState((), title, target)
}
Expand Down
7 changes: 7 additions & 0 deletions extras/js/src/main/scala/kreuzberg/extras/UriHelper.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package kreuzberg.extras

object UriHelper {
inline def encodeUriComponent(s: String): String = scalajs.js.URIUtils.encodeURIComponent(s)

inline def decodeUriComponent(s: String): String = scalajs.js.URIUtils.decodeURIComponent(s)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package kreuzberg.extras

object BrowserRouting {

def getCurrentPath(): String = {
"/"
def getCurrentResource(): UrlResource = {
UrlResource("")
}

def setDocumentTitle(title: String): Unit = {
// empty
}
Expand Down
10 changes: 10 additions & 0 deletions extras/jvm/src/main/scala/kreuzberg/extras/UriHelper.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package kreuzberg.extras

import java.net.{URLDecoder, URLEncoder}
import java.nio.charset.StandardCharsets

object UriHelper {
inline def encodeUriComponent(s: String): String = URLEncoder.encode(s, StandardCharsets.UTF_8)

inline def decodeUriComponent(s: String): String = URLDecoder.decode(s, StandardCharsets.UTF_8)
}
10 changes: 10 additions & 0 deletions extras/native/src/main/scala/kreuzberg/extras/UriHelper.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package kreuzberg.extras

import java.net.{URLDecoder, URLEncoder}

object UriHelper {
// Scala Native doesn't know the Charset here
inline def encodeUriComponent(s: String): String = URLEncoder.encode(s, "UTF-8")

inline def decodeUriComponent(s: String): String = URLDecoder.decode(s, "UTF-8")
}
68 changes: 49 additions & 19 deletions extras/shared/src/main/scala/kreuzberg/extras/PathCodec.scala
Original file line number Diff line number Diff line change
@@ -1,59 +1,89 @@
package kreuzberg.extras

/** Helper for encoding/decoding paths. */
/** Helper for encoding/decoding [[UrlResource]]. */
trait PathCodec[S] {
def handles(path: String): Boolean
def handles(resource: UrlResource): Boolean

def decode(path: String): Option[S]
def decode(resource: UrlResource): Option[S]

def forceDecode(path: String): S = decode(path).getOrElse {
def forceDecode(resource: UrlResource): S = decode(resource).getOrElse {
throw new IllegalStateException("Invalid path")
}

def encode(value: S): String
def encode(value: S): UrlResource
}

object PathCodec {

/** A Constant path. */
def const(constantPath: String): PathCodec[Unit] = new PathCodec[Unit] {
def const(path: String): PathCodec[Unit] = new PathCodec[Unit] {

override def handles(path: String): Boolean = path == constantPath
override def handles(resource: UrlResource): Boolean = resource.path == path

override def decode(path: String): Option[Unit] = if (path == constantPath) {
override def decode(resource: UrlResource): Option[Unit] = if (path == resource.path) {
Some(())
} else {
None
}

override def encode(value: Unit): String = constantPath
override def encode(value: Unit): UrlResource = UrlResource(path)
}

def constWithQueryParams(path: String, params: String*): PathCodec[Seq[String]] = new PathCodec[Seq[String]] {
override def handles(resource: UrlResource): Boolean = {
resource.path == path && {
val queryArgs = resource.queryArgs
params.forall(p => queryArgs.contains(p))
}
}

override def decode(resource: UrlResource): Option[Seq[String]] = {
if (resource.path != path) {
None
} else {
val builder = Seq.newBuilder[String]
val queryArgs = resource.queryArgs
val it = params.iterator
while (it.hasNext) {
queryArgs.get(it.next()) match {
case None => return None
case Some(v) => builder += v
}
}
Some(builder.result())
}
}

override def encode(value: Seq[String]): UrlResource = {
UrlResource.encodeWithArgs(path, params.zip(value))
}
}

/** A Simple Prefix. */
def prefix(prefix: String): PathCodec[String] = new PathCodec[String] {

override def handles(path: String): Boolean = path.startsWith(prefix)
override def handles(resource: UrlResource): Boolean = resource.path.startsWith(prefix)

override def decode(path: String): Option[String] = {
if (handles(prefix)) {
Some(path.stripPrefix(prefix))
override def decode(resource: UrlResource): Option[String] = {
if (handles(resource)) {
Some(resource.path.stripPrefix(prefix))
} else {
None
}
}

override def encode(value: String): String = {
prefix + value
override def encode(value: String): UrlResource = {
UrlResource(prefix + value)
}
}

/** Collects all. */
def all: PathCodec[String] = new PathCodec[String] {
def all: PathCodec[UrlResource] = new PathCodec[UrlResource] {

override def handles(path: String): Boolean = true
override def handles(path: UrlResource): Boolean = true

override def decode(path: String): Option[String] = Some(path)
override def decode(resource: UrlResource): Option[UrlResource] = Some(resource)

override def encode(value: String): String = value
override def encode(value: UrlResource): UrlResource = value
}
}
30 changes: 15 additions & 15 deletions extras/shared/src/main/scala/kreuzberg/extras/Route.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import kreuzberg._
trait Route {

/** Returns true if the route can handle a given path. */
def canHandle(path: String): Boolean
def canHandle(resource: UrlResource): Boolean

/** Title displayed while loading. */
def preTitle(path: String): String
def preTitle(resource: UrlResource): String

/** Execute the route, can load lazy. */
def target(path: String)(using AssemblerContext): Effect[RoutingTarget]
def target(resource: UrlResource)(using AssemblerContext): Effect[RoutingTarget]
}

case class RoutingTarget(
Expand All @@ -27,23 +27,23 @@ object Route {
type State
val pathCodec: PathCodec[State]

override def canHandle(path: String): Boolean = pathCodec.handles(path)
override def canHandle(resource: UrlResource): Boolean = pathCodec.handles(resource)

override def target(path: String)(using AssemblerContext): Effect[RoutingTarget] = Effect.const {
eagerTarget(path)
override def target(resource: UrlResource)(using AssemblerContext): Effect[RoutingTarget] = Effect.const {
eagerTarget(resource)
}

def eagerTarget(path: String): RoutingTarget = {
val state = pathCodec.decode(path).getOrElse {
throw new IllegalStateException(s"Unmatched path ${path}")
def eagerTarget(resource: UrlResource): RoutingTarget = {
val state = pathCodec.decode(resource).getOrElse {
throw new IllegalStateException(s"Unmatched path ${resource}")
}
RoutingTarget(title(state), component(state))
}

/** Returns a title for that path. */
def title(state: State): String

override def preTitle(path: String): String = eagerTarget(path).title
override def preTitle(resource: UrlResource): String = eagerTarget(resource).title

/** Assembles a component for a given path. */
def component(state: State): Component
Expand Down Expand Up @@ -85,14 +85,14 @@ object Route {
routingTarget: S => AssemblerContext ?=> Effect[RoutingTarget]
) extends Route {

override def canHandle(path: String): Boolean = pathCodec.handles(path)
override def canHandle(resource: UrlResource): Boolean = pathCodec.handles(resource)

override def target(path: String)(using AssemblerContext): Effect[RoutingTarget] = {
routingTarget(pathCodec.forceDecode(path))
override def target(resource: UrlResource)(using AssemblerContext): Effect[RoutingTarget] = {
routingTarget(pathCodec.forceDecode(resource))
}

override def preTitle(path: String): String = {
eagerTitle(pathCodec.forceDecode(path))
override def preTitle(resource: UrlResource): String = {
eagerTitle(pathCodec.forceDecode(resource))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ case class RouterLink(
override def assemble(implicit c: SimpleContext): Html = {
val link = PlainLink(name, target)
add(
link.onClick.to(SimpleRouter.gotoTarget(target))
link.onClick.to(SimpleRouter.gotoTarget(UrlResource(target)))
)
if (deco) {
span("[", link.wrap, "]")
Expand Down

0 comments on commit 24df084

Please sign in to comment.