Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
japgolly.scalajs.react.extra.router.Dsl.scala Maven / Gradle / Ivy
package japgolly.scalajs.react.extra.router
import japgolly.scalajs.react.extra.internal.RouterMacros
import japgolly.scalajs.react.internal.identityFn
import japgolly.scalajs.react.vdom.VdomElement
import java.util.UUID
import java.util.regex.{Matcher, Pattern}
import org.scalajs.dom.window.console
import scala.reflect.ClassTag
import scala.scalajs.js.URIUtils._
import scala.util.matching.Regex
/**
* This is not meant to be imported by library-users;
* [[RouterConfigDsl]] is the entire library-user-facing facade & DSL.
*/
object StaticDsl {
private val regexEscape1 = """([-()\[\]{}+?*.$\^|,:# A
val gb: C => B
val gc: (A, B) => C
def apply(fa: RouteB[A], fb: RouteB[B]): RouteB[C] =
new RouteB(
fa.regex + fb.regex,
fa.matchGroups + fb.matchGroups,
g => for {a <- fa.parse(g); b <- fb.parse(i => g(i + fa.matchGroups))} yield gc(a, b),
c => fa.build(ga(c)) + fb.build(gb(c)))
}
trait Composition_PriLowest {
implicit def ***[A, B] = Composition[A, B, (A, B)](_._1, _._2, (_, _))
}
trait Composition_PriLow extends Composition_PriLowest {
// Generated by bin/gen-router
implicit def T3[A,B,C] = Composition[(A,B), C, (A,B,C)](r => (r._1,r._2), _._3, (l,r) => (l._1,l._2,r))
implicit def T4[A,B,C,D] = Composition[(A,B,C), D, (A,B,C,D)](r => (r._1,r._2,r._3), _._4, (l,r) => (l._1,l._2,l._3,r))
implicit def T5[A,B,C,D,E] = Composition[(A,B,C,D), E, (A,B,C,D,E)](r => (r._1,r._2,r._3,r._4), _._5, (l,r) => (l._1,l._2,l._3,l._4,r))
implicit def T6[A,B,C,D,E,F] = Composition[(A,B,C,D,E), F, (A,B,C,D,E,F)](r => (r._1,r._2,r._3,r._4,r._5), _._6, (l,r) => (l._1,l._2,l._3,l._4,l._5,r))
implicit def T7[A,B,C,D,E,F,G] = Composition[(A,B,C,D,E,F), G, (A,B,C,D,E,F,G)](r => (r._1,r._2,r._3,r._4,r._5,r._6), _._7, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,r))
implicit def T8[A,B,C,D,E,F,G,H] = Composition[(A,B,C,D,E,F,G), H, (A,B,C,D,E,F,G,H)](r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7), _._8, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,r))
implicit def T9[A,B,C,D,E,F,G,H,I] = Composition[(A,B,C,D,E,F,G,H), I, (A,B,C,D,E,F,G,H,I)](r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7,r._8), _._9, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,l._8,r))
implicit def T10[A,B,C,D,E,F,G,H,I,J] = Composition[(A,B,C,D,E,F,G,H,I), J, (A,B,C,D,E,F,G,H,I,J)](r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7,r._8,r._9), _._10, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,l._8,l._9,r))
implicit def T11[A,B,C,D,E,F,G,H,I,J,K] = Composition[(A,B,C,D,E,F,G,H,I,J), K, (A,B,C,D,E,F,G,H,I,J,K)](r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7,r._8,r._9,r._10), _._11, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,l._8,l._9,l._10,r))
implicit def T12[A,B,C,D,E,F,G,H,I,J,K,L] = Composition[(A,B,C,D,E,F,G,H,I,J,K), L, (A,B,C,D,E,F,G,H,I,J,K,L)](r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7,r._8,r._9,r._10,r._11), _._12, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,l._8,l._9,l._10,l._11,r))
implicit def T13[A,B,C,D,E,F,G,H,I,J,K,L,M] = Composition[(A,B,C,D,E,F,G,H,I,J,K,L), M, (A,B,C,D,E,F,G,H,I,J,K,L,M)](r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7,r._8,r._9,r._10,r._11,r._12), _._13, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,l._8,l._9,l._10,l._11,l._12,r))
implicit def T14[A,B,C,D,E,F,G,H,I,J,K,L,M,N] = Composition[(A,B,C,D,E,F,G,H,I,J,K,L,M), N, (A,B,C,D,E,F,G,H,I,J,K,L,M,N)](r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7,r._8,r._9,r._10,r._11,r._12,r._13), _._14, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,l._8,l._9,l._10,l._11,l._12,l._13,r))
implicit def T15[A,B,C,D,E,F,G,H,I,J,K,L,M,N,O] = Composition[(A,B,C,D,E,F,G,H,I,J,K,L,M,N), O, (A,B,C,D,E,F,G,H,I,J,K,L,M,N,O)](r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7,r._8,r._9,r._10,r._11,r._12,r._13,r._14), _._15, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,l._8,l._9,l._10,l._11,l._12,l._13,l._14,r))
implicit def T16[A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P] = Composition[(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O), P, (A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P)](r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7,r._8,r._9,r._10,r._11,r._12,r._13,r._14,r._15), _._16, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,l._8,l._9,l._10,l._11,l._12,l._13,l._14,l._15,r))
implicit def T17[A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q] = Composition[(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P), Q, (A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q)](r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7,r._8,r._9,r._10,r._11,r._12,r._13,r._14,r._15,r._16), _._17, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,l._8,l._9,l._10,l._11,l._12,l._13,l._14,l._15,l._16,r))
implicit def T18[A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R] = Composition[(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q), R, (A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R)](r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7,r._8,r._9,r._10,r._11,r._12,r._13,r._14,r._15,r._16,r._17), _._18, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,l._8,l._9,l._10,l._11,l._12,l._13,l._14,l._15,l._16,l._17,r))
implicit def T19[A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S] = Composition[(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R), S, (A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S)](r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7,r._8,r._9,r._10,r._11,r._12,r._13,r._14,r._15,r._16,r._17,r._18), _._19, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,l._8,l._9,l._10,l._11,l._12,l._13,l._14,l._15,l._16,l._17,l._18,r))
implicit def T20[A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T] = Composition[(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S), T, (A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T)](r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7,r._8,r._9,r._10,r._11,r._12,r._13,r._14,r._15,r._16,r._17,r._18,r._19), _._20, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,l._8,l._9,l._10,l._11,l._12,l._13,l._14,l._15,l._16,l._17,l._18,l._19,r))
implicit def T21[A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U] = Composition[(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T), U, (A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U)](r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7,r._8,r._9,r._10,r._11,r._12,r._13,r._14,r._15,r._16,r._17,r._18,r._19,r._20), _._21, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,l._8,l._9,l._10,l._11,l._12,l._13,l._14,l._15,l._16,l._17,l._18,l._19,l._20,r))
implicit def T22[A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V] = Composition[(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U), V, (A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V)](r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7,r._8,r._9,r._10,r._11,r._12,r._13,r._14,r._15,r._16,r._17,r._18,r._19,r._20,r._21), _._22, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,l._8,l._9,l._10,l._11,l._12,l._13,l._14,l._15,l._16,l._17,l._18,l._19,l._20,l._21,r))
}
trait Composition_PriMed extends Composition_PriLow {
implicit def _toA[A] = Composition[Unit, A, A](_ => (), identityFn, (_, a) => a)
implicit def Ato_[A] = Composition[A, Unit, A](identityFn, _ => (), (a, _) => a)
}
object Composition extends Composition_PriMed {
implicit def _to_ = Composition[Unit, Unit, Unit](_ => (), _ => (), (_, _) => ())
type Aux[A, B, O] = Composition[A, B] {type C = O}
def apply[A, B, O](a: O => A, b: O => B, c: (A, B) => O): Aux[A, B, O] =
new Composition[A, B] {
override type C = O
val ga = a
val gb = b
val gc = c
}
}
private val someUnit = Some(())
def literal(s: String): RouteB[Unit] =
new RouteB(regexEscape(s), 0, _ => someUnit, _ => s)
val / = literal("/")
}
abstract class RouteCommon[R[X] <: RouteCommon[R, X], A] {
def parseThen(f: Option[A] => Option[A]): R[A]
/**
* Prism map.
*
* Some values of `A` can be turned into a `B`s, some fail (in which case the route is considered non-matching).
*
* All `B`s can be turned back into `A`s.
*/
def pmap[B](b: A => Option[B])(a: B => A): R[B]
/**
* Exponential map.
*
* Any `A` can be turned into a `B` and vice versa.
*/
final def xmap[B](b: A => B)(a: B => A): R[B] =
pmap(a => Some(b(a)))(a)
final def filter(f: A => Boolean): R[A] =
parseThen(_ filter f)
final def mapParsed[B <: A](f: A => B): R[B] =
xmap(f)(x => x)
final def mapInput[B >: A](f: B => A): R[B] =
xmap[B](x => x)(f)
final def const[B](b: B)(implicit ev: A =:= Unit, ev2: Unit =:= A): R[B] =
xmap(_ => b)(_ => ())
}
/**
* A fragment of a route. Can be composed with other fragments.
*
* @param matchGroups The number of matches that `regex` will capture.
*/
class RouteB[A](val regex: String,
val matchGroups: Int,
val parse: (Int => String) => Option[A],
val build: A => String) extends RouteCommon[RouteB, A] {
import RouteB.Composition
override def toString =
s"RouteB($regex)"
def ~[B](next: RouteB[B])(implicit c: Composition[A, B]): RouteB[c.C] =
c(this, next)
def /[B](next: RouteB[B])(implicit c: Composition[A, B]): RouteB[c.C] =
this ~ RouteB./ ~ next
override def parseThen(f: Option[A] => Option[A]): RouteB[A] =
new RouteB(regex, matchGroups, f compose parse, build)
override def pmap[B](b: A => Option[B])(a: B => A): RouteB[B] =
new RouteB(regex, matchGroups, parse(_) flatMap b, build compose a)
/**
* Maps the captures values of the route to a case class.
*/
def caseClass[B]: RouteB[B] =
macro RouterMacros.quietCaseClassB[B]
/**
* Same as [[caseClass]] except the code generated by the macro is printed to stdout.
*/
def caseClassDebug[B]: RouteB[B] =
macro RouterMacros.debugCaseClassB[B]
def option: RouteB[Option[A]] =
new RouteB[Option[A]](s"($regex)?", matchGroups + 1,
g => Some(if (g(0) eq null) None else parse(i => g(i + 1))),
_.fold("")(build))
final def route: Route[A] = {
val p = Pattern.compile("^" + regex + "$")
// https://github.com/scala-js/scala-js/issues/1727
// val g = p.matcher("").groupCount
// if (g != matchGroups)
// sys.error(s"Error in regex: /${p.pattern}/. Expected $matchGroups match groups but detected $g.")
new Route(p, m => parse(i => m.group(i + 1)), a => Path(build(a)))
}
}
class RouteBO[A](private val r: RouteB[Option[A]]) extends AnyVal {
/**
* Specify a default value when parsing.
*
* Note: Unlike [[withDefault()]] path generation will still explicitly include the default value.
*
* Eg. If the path is like "/file[.format]" and the default is JSON, "/file" will be read as "/file.json", but
* when generating a path with JSON this will generate "/file.json" instead of "/file".
*/
def parseDefault(default: => A): RouteB[A] =
r.xmap(_ getOrElse default)(Some(_))
/**
* Specify a default value.
*
* Note: Unlike [[parseDefault()]] this will affect path generation too.
*
* Eg. If the path is like "/file[.format]" and the default is JSON, "/file" will be read as "/file.json", and
* when generating a path with JSON this will generate "/file" instead of "/file.json".
*
* Make sure the type has a useful `.equals()` implementation.
* Example: `default == default` should be `true`.
*/
def withDefault(default: => A): RouteB[A] =
r.xmap(_ getOrElse default)(a => if (default == a) None else Some(a))
}
/**
* A complete route.
*/
final class Route[A](pattern: Pattern,
parseFn: Matcher => Option[A],
buildFn: A => Path) extends RouteCommon[Route, A] {
override def toString =
s"Route($pattern)"
override def parseThen(f: Option[A] => Option[A]): Route[A] =
new Route(pattern, f compose parseFn, buildFn)
override def pmap[B](b: A => Option[B])(a: B => A): Route[B] =
new Route(pattern, parseFn(_) flatMap b, buildFn compose a)
/**
* Maps the captures values of the route to a case class.
*/
def caseClass[B]: Route[B] =
macro RouterMacros.quietCaseClass[B]
/**
* Same as [[caseClass]] except the code generated by the macro is printed to stdout.
*/
def caseClassDebug[B]: Route[B] =
macro RouterMacros.debugCaseClass[B]
def parse(path: Path): Option[A] = {
val m = pattern.matcher(path.value)
if (m.matches)
parseFn(m)
else
None
}
def pathFor(a: A): Path =
buildFn(a)
}
// ===================================================================================================================
final class DynamicRouteB[Page, P <: Page, O](private val f: (P => Action[Page]) => O) extends AnyVal {
def ~>(g: P => Action[Page]): O = f(g)
}
final class StaticRouteB[Page, O](private val f: (=> Action[Page]) => O) extends AnyVal {
def ~>(a: => Action[Page]): O = f(a)
}
final class StaticRedirectB[Page, O](private val f: (=> Redirect[Page]) => O) extends AnyVal {
def ~>(a: => Redirect[Page]): O = f(a)
}
final class DynamicRedirectB[Page, A, O](private val f: (A => Redirect[Page]) => O) extends AnyVal {
def ~>(a: A => Redirect[Page]): O = f(a)
}
}
// =====================================================================================================================
// =====================================================================================================================
object RouterConfigDsl {
def apply[Page] =
new BuildInterface[Page, Unit]
class BuildInterface[Page, Props] {
def use[A](f: RouterConfigDsl[Page, Props] => A): A =
f(new RouterConfigDsl)
def buildConfig(f: RouterConfigDsl[Page, Props] => RouterWithPropsConfig[Page, Props]): RouterWithPropsConfig[Page, Props] =
use(f)
def buildRule(f: RouterConfigDsl[Page, Props] => RoutingRule[Page, Props]): RoutingRule[Page, Props] =
use(f)
}
}
object RouterWithPropsConfigDsl {
def apply[Page, Props] =
new RouterConfigDsl.BuildInterface[Page, Props]
}
/**
* DSL for creating [[RouterConfig]].
*
* Instead creating an instance of this yourself, use [[RouterConfigDsl.apply]].
*/
final class RouterConfigDsl[Page, Props] {
import StaticDsl._
type Action = japgolly.scalajs.react.extra.router.Action[Page]
type Renderer = japgolly.scalajs.react.extra.router.Renderer[Page, Props]
type Redirect = japgolly.scalajs.react.extra.router.Redirect[Page]
type Parsed = RouterConfig.Parsed[Page]
// -------------------------------------------------------------------------------------------------------------------
// Route DSL
private def uuidRegex = "([A-Fa-f0-9]{8}(?:-[A-Fa-f0-9]{4}){3}-[A-Fa-f0-9]{12})"
def root = Path.root
val int = new RouteB[Int] ("(-?\\d+)", 1, g => Some(g(0).toInt), _.toString)
val long = new RouteB[Long]("(-?\\d+)", 1, g => Some(g(0).toLong), _.toString)
val uuid = new RouteB[UUID](uuidRegex, 1, g => Some(UUID fromString g(0)), _.toString)
private def __string1(regex: String): RouteB[String] =
new RouteB(regex, 1, g => Some(g(0)), identityFn)
/**
* Matches a string.
*
* Best to use a whitelist of characters, eg. "[a-zA-Z0-9]+".
* Do not capture groups; use "[a-z]+" instead of "([a-z]+)".
* If you need to group, use non-capturing groups like "(?:bye|hello)" instead of "(bye|hello)".
*/
def string(regex: String): RouteB[String] =
__string1("(" + regex + ")")
/** Captures the (non-empty) remaining portion of the URL path. */
def remainingPath: RouteB[String] =
__string1("(.+)$")
/** Captures the (potentially-empty) remaining portion of the URL path. */
def remainingPathOrBlank: RouteB[String] =
__string1("(.*)$")
implicit def _ops_for_routeb_option[A](r: RouteB[Option[A]]) = new RouteBO(r)
implicit def _auto_routeB_from_str(l: String) = RouteB.literal(l)
implicit def _auto_routeB_from_path(p: Path) = RouteB.literal(p.value)
implicit def _auto_route_from_routeB[A, R](r: R)(implicit ev: R => RouteB[A]) = r.route
// -------------------------------------------------------------------------------------------------------------------
// Action DSL
implicit def _auto_someAction[A <: Action](a: A): Option[A] = Some(a)
def render[A](a: => A)(implicit ev: A => VdomElement): Renderer =
Renderer(_ => _ => a)
def renderR[A](g: RouterCtl[Page] => A)(implicit ev: A => VdomElement): Renderer =
Renderer(r => _ => g(r))
def renderP[A](g: Props => A)(implicit ev: A => VdomElement): Renderer =
Renderer(_ => props => g(props))
def renderRP[A](g: (RouterCtl[Page], Props) => A)(implicit ev: A => VdomElement): Renderer =
Renderer(r => props => g(r, props))
def dynRender[P <: Page, A](g: P => A)(implicit ev: A => VdomElement): P => Renderer =
p => Renderer(_ => _ => g(p))
def dynRenderR[P <: Page, A](g: (P, RouterCtl[Page]) => A)(implicit ev: A => VdomElement): P => Renderer =
p => Renderer(r => _ => g(p, r))
def dynRenderP[P <: Page, A](g: (P, Props) => A)(implicit ev: A => VdomElement): P => Renderer =
p => Renderer(_ => props => g(p, props))
def dynRenderRP[P <: Page, A](g: (P, RouterCtl[Page], Props) => A)(implicit ev: A => VdomElement): P => Renderer =
p => Renderer(r => props => g(p, r, props))
def redirectToPage(page: Page)(implicit via: SetRouteVia): RedirectToPage[Page] =
RedirectToPage[Page](page, via)
def redirectToPath(path: Path)(implicit via: SetRouteVia): RedirectToPath[Page] =
RedirectToPath[Page](path, via)
def redirectToPath(path: String)(implicit via: SetRouteVia): RedirectToPath[Page] =
redirectToPath(Path(path))
// -------------------------------------------------------------------------------------------------------------------
// Rule building DSL
type Rule = japgolly.scalajs.react.extra.router.RoutingRule[Page, Props]
type Rules = japgolly.scalajs.react.extra.router.RoutingRule.WithFallback[Page, Props]
def emptyRule: Rule = RoutingRule.empty
implicit def _auto_parsed_from_redirect(r: Redirect): Parsed = Left(r)
implicit def _auto_parsed_from_page (p: Page) : Parsed = Right(p)
implicit def _auto_parsedO_from_parsed [A](p: A) (implicit ev: A => Parsed): Option[Parsed] = Some(p)
implicit def _auto_parsedO_from_parsedO[A](o: Option[A])(implicit ev: A => Parsed): Option[Parsed] = o.map(a => a)
implicit def _auto_notFound_from_parsed [A](a: A) (implicit ev: A => Parsed): Path => Parsed = _ => a
implicit def _auto_notFound_from_parsedF[A](f: Path => A)(implicit ev: A => Parsed): Path => Parsed = f(_)
implicit def _auto_routeParser_from_parsed [A](a: A) (implicit ev: A => Parsed): Path => Option[Parsed] = _ => Some(a)
implicit def _auto_routeParser_from_parsedF [A](f: Path => A) (implicit ev: A => Parsed): Path => Option[Parsed] = p => Some(f(p))
implicit def _auto_routeParser_from_parsedO [A](o: Option[A]) (implicit ev: A => Parsed): Path => Option[Parsed] = _ => o.map(a => a)
implicit def _auto_routeParser_from_parsedFO[A](f: Path => Option[A])(implicit ev: A => Parsed): Path => Option[Parsed] = f(_).map(a => a)
// allows dynamicRoute ~~> X to not care if X is (Action) or (P => Action)
implicit def _auto_pToAction_from_action(a: => Action): Page => Action = _ => a
implicit def _auto_rules_from_rulesB(r: Rule): Rules = r.noFallback
// Only really aids rewriteRuleR but safe anyway
implicit def _auto_pattern_from_regex(r: Regex): Pattern = r.pattern
/**
* Note: Requires that `Page#equals()` be sensible.
*/
def staticRoute(r: Route[Unit], page: Page): StaticRouteB[Page, Rule] = {
val dyn = dynamicRoute(r const page){ case p if page == p => p }
new StaticRouteB(a => dyn ~> a)
}
def dynamicRoute[P <: Page](r: Route[P])(pf: PartialFunction[Page, P]): DynamicRouteB[Page, P, Rule] =
dynamicRouteF(r)(pf.lift)
def dynamicRouteF[P <: Page](r: Route[P])(op: Page => Option[P]): DynamicRouteB[Page, P, Rule] = {
def onPage[A](f: P => A)(page: Page): Option[A] =
op(page) map f
new DynamicRouteB(a => RoutingRule.Atom(r.parse, onPage(r.pathFor), (_, p) => onPage(a)(p)))
}
def dynamicRouteCT[P <: Page](r: Route[P])(implicit ct: ClassTag[P]): DynamicRouteB[Page, P, Rule] =
dynamicRouteF(r)(ct.unapply)
def staticRedirect(r: Route[Unit]): StaticRedirectB[Page, Rule] =
new StaticRedirectB(a => rewritePathF(r.parse(_) map (_ => a)))
def dynamicRedirect[A](r: Route[A]): DynamicRedirectB[Page, A, Rule] =
new DynamicRedirectB(f => rewritePathF(r.parse(_) map f))
def rewritePath(pf: PartialFunction[Path, Redirect]): Rule =
rewritePathF(pf.lift)
def rewritePathF(f: Path => Option[Redirect]): Rule =
RoutingRule parseOnly f
def rewritePathR(r: Pattern, f: Matcher => Option[Redirect]): Rule =
rewritePathF { p =>
val m = r.matcher(p.value)
if (m.matches) f(m) else None
}
/** Captures the query portion of the URL to a param map.
*
* Note that this is not a strict capture, URLs without a query string will still be accepted,
* and the parameter map will simply by empty.
*/
lazy val queryToMap: RouteB[Map[String, String]] = {
type A = Map[String, String]
val queryRegex = """(\?[^#]*)?"""
val kvRegex = "^([^=]+)(?:=(.*))?$".r
def decode(str: String): String =
decodeURIComponent(str.replace('+', ' '))
val needingEncoding = """[~!'()]|%[02]0""".r
def encode(str: String): String =
needingEncoding.replaceAllIn(encodeURIComponent(str), m =>
m.group(0) match {
case "%20" => "+"
case "!" => "%21"
case "'" => "%27"
case "(" => "%28"
case ")" => "%29"
case "~" => "%7E"
case "%00" => (0: Char).toString
}
)
val parse: (Int => String) => Option[A] =
_(0) match {
case null => Some(Map.empty)
case q =>
q
.tail
.split("&")
.iterator
.filter(_.nonEmpty)
.map {
case kvRegex(k, null) => Some(decode(k) -> "")
case kvRegex(k, v) => Some(decode(k) -> decode(v))
case x =>
console.warn(s"Unable to parse query string pair: $x")
None
}
.foldLeft(Option(Map.empty: A)) {
case (Some(m), Some(kv)) => Some(m + kv)
case _ => None
}
}
val build: A => String =
m =>
if (m.isEmpty)
""
else
m.iterator
.map {
case (k, "") => encode(k)
case (k, v) => s"${encode(k)}=${encode(v)}"
}
.mkString("?", "&", "")
new RouteB[Map[String, String]](queryRegex, 1, parse, build)
}
// -------------------------------------------------------------------------------------------------------------------
// Utilities
/**
* Removes the query portion of the URL.
*
* e.g. `a/b?c=1` to `a/b`
*/
def removeQuery: Rule =
rewritePathR("^(.*?)\\?.*$".r, m => redirectToPath(m group 1)(SetRouteVia.HistoryReplace))
/**
* A rule that uses a replace-state redirect to remove trailing slashes from route URLs.
*/
def removeTrailingSlashes: Rule =
rewritePathR("^(.*?)/+$".r, m => redirectToPath(m group 1)(SetRouteVia.HistoryReplace))
/**
* A rule that uses a replace-state redirect to remove leading slashes from route URLs.
*/
def removeLeadingSlashes: Rule =
rewritePathR("^/+(.*)$".r, m => redirectToPath(m group 1)(SetRouteVia.HistoryReplace))
/**
* A rule that uses a replace-state redirect to remove leading and trailing slashes from route URLs.
*/
def trimSlashes: Rule = (
rewritePathR("^/*(.*?)/+$".r, m => redirectToPath(m group 1)(SetRouteVia.HistoryReplace))
| removeLeadingSlashes)
}