All Downloads are FREE. Search and download functionalities are using the official Maven repository.

japgolly.scalajs.react.extra.router.RoutingRule.scala Maven / Gradle / Ivy

package japgolly.scalajs.react.extra.router

import japgolly.scalajs.react.extra.router.RouterConfig.Parsed
import japgolly.scalajs.react.util.DefaultEffects
import japgolly.scalajs.react.util.Effect.Sync
import scala.reflect.ClassTag
import scala.scalajs.js

/** A single routing rule. Intended to be composed with other [[RoutingRule]]s.
  * When all rules are composed, this is turned into a [[RoutingRule.WithFallback]] instance.
  *
  * @tparam Page The type of legal pages. Most commonly, a sealed trait that you've created, where all subclasses
  *              represent a page in your SPA.
  */
sealed trait RoutingRule[Page, Props] {

  /** Compose rules. */
  final def |(that: RoutingRule[Page, Props]): RoutingRule[Page, Props] =
    RoutingRule.Or(this, that)

  def xmap[A](f: Page => A)(g: A => Page): RoutingRule[A, Props]

  final def pmap[W](f: Page => W)(pf: PartialFunction[W, Page]): RoutingRule[W, Props] =
    pmapF(f)(pf.lift)

  final def pmapCT[W](f: Page => W)(implicit ct: ClassTag[Page]): RoutingRule[W, Props] =
    pmapF(f)(ct.unapply)

  def pmapF[W](f: Page => W)(g: W => Option[Page]): RoutingRule[W, Props]

  final def widen[W >: Page](pf: PartialFunction[W, Page]): RoutingRule[W, Props] =
    widenF(pf.lift)

  final def widenCT[W >: Page](implicit ct: ClassTag[Page]): RoutingRule[W, Props] =
    widenF(ct.unapply)

  final def widenF[W >: Page](f: W => Option[Page]): RoutingRule[W, Props] =
    pmapF[W](p => p)(f)

  /** See [[autoCorrect()]]. */
  final def autoCorrect: RoutingRule[Page, Props] =
    autoCorrect(SetRouteVia.HistoryReplace)

  /**
    * When a route matches a page, compare its [[Path]] to what the route would generate for the same page and if they
    * differ, redirect to the generated one.
    *
    * Example: If a route matches `/issue/dev-23` and returns a `Page("DEV", 23)` for which the generate path would be
    * `/issue/DEV-23`, this would automatically redirect `/issue/dev-23` to `/issue/DEV-23`, and process
    * `/issue/DEV-23` normally using its associated action.
    */
  final def autoCorrect(redirectVia: SetRouteVia): RoutingRule[Page, Props] =
    RoutingRule.AutoCorrect(this, redirectVia)

  /** Modify the path(es) generated and parsed by this rule.
    *
    * @param onCreate Modify paths when generating for a route.
    * @param onParse When parsing a path, transform and optionally reject it.
    */
  def modPath(onCreate: Path => Path, onParse: Path => Option[Path]): RoutingRule[Page, Props]

  /** Add a prefix to the path(es) generated and parsed by this rule. */
  final def prefixPath(prefix: String): RoutingRule[Page, Props] =
    modPath(
      p => Path(prefix + p.value),
      _ removePrefix prefix)

  /** Add a prefix to the path(es) generated and parsed by this rule.
    *
    * Unlike [[prefixPath()]] when the suffix is non-empty, a slash is added between prefix and suffix.
    */
  final def prefixPath_/(prefix: String): RoutingRule[Page, Props] = {
    val pre = Path(prefix)
    modPath(
      p => if (p.isEmpty) pre else pre / p,
      p => if (p.value == prefix) Some(Path.root) else p.removePrefix(prefix + "/"))
  }

  /** Prevent this rule from functioning unless some condition holds.
    * When the condition doesn't hold, an optional fallback action may be performed.
    *
    * @param condition Sync[Unit] that requested page and returns true if the page should be rendered.
    * @param fallback  Response when rule matches but condition doesn't hold.
    *                  If response is `None` it will be as if this rule doesn't exist and will likely end in the
    *                  route-not-found fallback behaviour.
    */
  final def addConditionWithOptionalFallback[G[_]](condition: G[Boolean], fallback: Page => Option[Action[Page, Props]])(implicit G: Sync[G]): RoutingRule[Page, Props] =
    RoutingRule.Conditional(G.toJsFn(condition), this, fallback)

  /** Prevent this rule from functioning unless some condition holds, passes in the page
    * requested as part of the context.
    * When the condition doesn't hold, an optional fallback action may be performed.
    *
    * @param condition Function that takes the requested page and returns true if the page should be rendered.
    * @param fallback  Response when rule matches but condition doesn't hold.
    *                  If response is `None` it will be as if this rule doesn't exist and will likely end in the
    *                  route-not-found fallback behaviour.
    */
  final def addConditionWithOptionalFallbackBy[G[_]](condition: Page => G[Boolean], fallback: Page => Option[Action[Page, Props]])(implicit G: Sync[G]): RoutingRule[Page, Props] =
    RoutingRule.ConditionalP(p => G.toJsFn(condition(p)), this, fallback)

  /** Prevent this rule from functioning unless some condition holds.
    * When the condition doesn't hold, an optional fallback action may be performed.
    *
    * @param condition Sync[Unit] that requested page and returns true if the page should be rendered.
    * @param fallback  Response when rule matches but condition doesn't hold.
    *                  If response is `None` it will be as if this rule doesn't exist and will likely end in the
    *                  route-not-found fallback behaviour.
    */
  final def addConditionWithOptionalFallback[G[_]](condition: G[Boolean], fallback: Option[Action[Page, Props]])(implicit G: Sync[G]): RoutingRule[Page, Props] =
    RoutingRule.Conditional(G.toJsFn(condition), this, (_: Page) => fallback)

  /** Prevent this rule from functioning unless some condition holds, passes in the page
    * requested as part of the context.
    * When the condition doesn't hold, an optional fallback action may be performed.
    *
    * @param condition Function that takes the requested page and returns true if the page should be rendered.
    * @param fallback  Response when rule matches but condition doesn't hold.
    *                  If response is `None` it will be as if this rule doesn't exist and will likely end in the
    *                  route-not-found fallback behaviour.
    */
  final def addConditionWithOptionalFallbackBy[G[_]](condition: Page => G[Boolean], fallback: Option[Action[Page, Props]])(implicit G: Sync[G]): RoutingRule[Page, Props] =
    addConditionWithOptionalFallbackBy(condition, (_: Page) => fallback)

  /** Prevent this rule from functioning unless some condition holds.
    * When the condition doesn't hold, a fallback action is performed.
    *
    * @param condition Sync[Unit] that requested page and returns true if the page should be rendered.
    * @param fallback  Response when rule matches but condition doesn't hold.
    */
  final def addConditionWithFallback[G[_]](condition: G[Boolean], fallback: Action[Page, Props])(implicit G: Sync[G]): RoutingRule[Page, Props] =
    addConditionWithOptionalFallback(condition, (_: Page) => Some(fallback))

  /** Prevent this rule from functioning unless some condition holds, passes in the page
    * requested as part of the context.
    * When the condition doesn't hold, a fallback action is performed.
    *
    * @param condition Function that takes the requested page and returns true if the page should be rendered.
    * @param fallback  Response when rule matches but condition doesn't hold.
    */
  final def addConditionWithFallbackBy[G[_]](condition: Page => G[Boolean], fallback: Action[Page, Props])(implicit G: Sync[G]): RoutingRule[Page, Props] =
    addConditionWithOptionalFallbackBy(condition, (_: Page) => Some(fallback))

  /** Prevent this rule from functioning unless some condition holds.
    *
    * @param condition Sync[Unit] that requested page and returns true if the page should be rendered.
    */
  final def addCondition[G[_]](condition: G[Boolean])(implicit G: Sync[G]): RoutingRule[Page, Props] =
    addConditionWithOptionalFallback(condition, (_: Page) => None)

  /** Prevent this rule from functioning unless some condition holds, passes in the page
    * requested as part of the context.
    *
    * @param condition Function that takes the requested page and returns true if the page should be rendered.
    */
  final def addConditionBy[G[_]](condition: Page => G[Boolean])(implicit G: Sync[G]): RoutingRule[Page, Props] =
    addConditionWithOptionalFallbackBy(condition, (_: Page) => None)

  /** Specify behaviour when a `Page` doesn't have an associated `Path` or `Action`. */
  final def fallback(fallbackPath  : Page => Path,
                     fallbackAction: (Path, Page) => Action[Page, Props]): RoutingRule.WithFallback[Page, Props] =
    RoutingRule.WithFallbackF(this, fallbackPath, fallbackAction)(DefaultEffects.Sync)

  /** When a `Page` doesn't have an associated  `Path` or `Action`, throw a runtime error.
    *
    * This is the trade-off for keeping the parsing and generation of known `Page`s in sync - compiler proof of
    * `Page` exhaustiveness is sacrificed.
    *
    * It is recommended that you call [[RouterConfig.verify]] as a sanity-check.
    */
  final def noFallback: RoutingRule.WithFallback[Page, Props] =
    fallback(
      page         => sys error s"Unspecified path for page $page.",
      (path, page) => sys error s"Unspecified action for page $page at $path.")
}

object RoutingRule {

  // ===================================================================================================================

  /** @param parse  Attempt to parse a given path.
    * @param path   Attempt to determine the path for some page.
    * @param action Attempt to determine the action when a route resolves to some page.
    */
  final case class Atom[Page, Props](parse : Path         => Option[Parsed[Page]],
                                     path  : Page         => Option[Path],
                                     action: (Path, Page) => Option[Action[Page, Props]]) extends RoutingRule[Page, Props] {

    override def xmap[A](f: Page => A)(g: A => Page): RoutingRule[A, Props] =
      Atom[A, Props](
        p => parse(p).map(_.bimap(_ map f, f)),
        path compose g,
        (u, p) => action(u, g(p)).map(_ map f))

    override def pmapF[W](f: Page => W)(g: W => Option[Page]): RoutingRule[W, Props] =
      Atom[W, Props](
        parse(_).map(_.bimap(_ map f, f)),
        g(_).flatMap(path),
        (path, w) => g(w).flatMap(action(path, _)).map(_ map f))

    override def modPath(onCreate: Path => Path, onParse: Path => Option[Path]): RoutingRule[Page, Props] =
      Atom(
        onParse(_) flatMap parse,
        path(_) map onCreate,
        action)
  }

  // ===================================================================================================================

  final case class Conditional[Page, Props](condition : js.Function0[Boolean],
                                            underlying: RoutingRule[Page, Props],
                                            otherwise : Page => Option[Action[Page, Props]]) extends RoutingRule[Page, Props] {

    override def xmap[A](f: Page => A)(g: A => Page): RoutingRule[A, Props] =
      Conditional[A, Props](
        condition,
        underlying.xmap(f)(g),
        a => otherwise(g(a)).map(_.map(f)))

    override def pmapF[W](f: Page => W)(g: W => Option[Page]): RoutingRule[W, Props] =
      Conditional[W, Props](
        condition,
        underlying.pmapF(f)(g),
        g(_).flatMap(otherwise(_).map(_.map(f))))

    override def modPath(onCreate: Path => Path, onParse: Path => Option[Path]): RoutingRule[Page, Props] =
      copy(underlying = underlying.modPath(onCreate, onParse))
  }

  // ===================================================================================================================

  final case class ConditionalP[Page, Props](condition : Page => js.Function0[Boolean],
                                             underlying: RoutingRule[Page, Props],
                                             otherwise : Page => Option[Action[Page, Props]]) extends RoutingRule[Page, Props] {

    override def xmap[A](f: Page => A)(g: A => Page): RoutingRule[A, Props] =
      ConditionalP[A, Props](
        condition compose g,
        underlying.xmap(f)(g),
        a => otherwise(g(a)).map(_.map(f)))

    override def pmapF[W](f: Page => W)(g: W => Option[Page]): RoutingRule[W, Props] =
      ConditionalP[W, Props](
        g(_).fold[js.Function0[Boolean]](() => false)(condition),
        underlying.pmapF(f)(g),
        g(_).flatMap(otherwise(_).map(_.map(f))))

    override def modPath(onCreate: Path => Path, onParse: Path => Option[Path]): RoutingRule[Page, Props] =
      copy(underlying = underlying.modPath(onCreate, onParse))
  }

  // ===================================================================================================================

  final case class Or[Page, Props](lhs: RoutingRule[Page, Props], rhs: RoutingRule[Page, Props]) extends RoutingRule[Page, Props] {
    private def mod[A](f: RoutingRule[Page, Props] => RoutingRule[A, Props]): RoutingRule[A, Props] =
      Or(f(lhs), f(rhs))

    override def xmap[A](f: Page => A)(g: A => Page): RoutingRule[A, Props] =
      mod(_.xmap(f)(g))

    override def pmapF[W](f: Page => W)(g: W => Option[Page]): RoutingRule[W, Props] =
      mod(_.pmapF(f)(g))

    override def modPath(onCreate: Path => Path, onParse: Path => Option[Path]): RoutingRule[Page, Props] =
      mod(_.modPath(onCreate, onParse))
  }

  // ===================================================================================================================

  final case class AutoCorrect[Page, Props](underlying : RoutingRule[Page, Props],
                                            redirectVia: SetRouteVia) extends RoutingRule[Page, Props] {

    override def xmap[A](f: Page => A)(g: A => Page): RoutingRule[A, Props] =
      copy(underlying.xmap(f)(g))

    override def pmapF[W](f: Page => W)(g: W => Option[Page]): RoutingRule[W, Props] =
      copy(underlying.pmapF(f)(g))

    override def modPath(onCreate: Path => Path, onParse: Path => Option[Path]): RoutingRule[Page, Props] =
      copy(underlying.modPath(onCreate, onParse))
  }

  def parseOnly[Page, C](parse: Path => Option[Parsed[Page]]) =
    Atom[Page, C](parse, _ => None, (_, _) => None)

  def empty[P, C]: RoutingRule[P, C] =
    Atom(_ => None, _ => None, (_, _) => None)

  // ===================================================================================================================

  type WithFallback[Page, Props] = WithFallbackF[DefaultEffects.Sync, Page, Props]

  /** Exhaustive routing rules. For all `Page`s there are `Path`s and `Action`s. */
  final case class WithFallbackF[F[_], Page, Props](rule          : RoutingRule[Page, Props],
                                                    fallbackPath  : Page => Path,
                                                    fallbackAction: (Path, Page) => ActionF[F, Page, Props],
                                                   )(implicit F: Sync[F]) {

    /** Specify a catch-all response to unmatched/invalid routes. */
    def notFound(whenNotFound: Path => Parsed[Page]): RouterWithPropsConfigF[F, Page, Props] =
      notFoundDynamic(p => F.delay(whenNotFound(p)))

    /** Specify a catch-all response to unmatched/invalid routes. */
    def notFoundDynamic(whenNotFound: Path => F[Parsed[Page]]): RouterWithPropsConfigF[F, Page, Props] = {
      val rules = RoutingRulesF.fromRule(rule, fallbackPath, fallbackAction, whenNotFound)
      RouterConfig.withDefaults(rules)
    }
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy