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

urldsl.language.PathSegment.scala Maven / Gradle / Ivy

The newest version!
package urldsl.language

import urldsl.errors.{DummyError, ErrorFromThrowable, PathMatchingError, SimplePathMatchingError}
import urldsl.url.{UrlStringDecoder, UrlStringGenerator, UrlStringParserGenerator}
import urldsl.vocabulary._

import app.tulz.tuplez.Composition

import scala.language.implicitConversions

/** Represents a part of the path string of an URL, containing an information of type T, or an error of type A.
  * @tparam T
  *   type represented by this PathSegment
  * @tparam A
  *   type of the error that this PathSegment produces on "illegal" url paths.
  */
trait PathSegment[T, A] extends UrlPart[T, A] {

  /** Tries to match the list of [[urldsl.vocabulary.Segment]]s to create an instance of `T`. If it can not, it returns
    * an error indicating the reason of the failure. If it could, it returns the value of `T`, as well as the list of
    * unused segments.
    *
    * @example
    *   For example, a segment that matches simply a String in the first segment, when giving segments like
    *   List(Segment("hello"), Segment("3")) will return Right(PathMatchOutput("hello", List(Segment("3")))
    *
    * @param segments
    *   The list of [[urldsl.vocabulary.Segment]] to match this path segment again.
    * @return
    *   The "de-serialized" element with unused segment, if successful.
    */
  def matchSegments(segments: List[Segment]): Either[A, PathMatchOutput[T]]

  protected implicit def errorImpl: PathMatchingError[A]

  /** Tries to match the provided list of [[urldsl.vocabulary.Segment]]s to create an instance of `T`.
    *
    * If it can't, it returns an error indicating the reason of the failure. If it can but there are unused segments
    * left over, it fails with a `endOfSegmentRequired` error. If it can, it returns the output.
    *
    * It is thus similar to [[matchSegments]], but requiring that all segments have been consumed.
    *
    * @see
    *   [[matchSegments]]
    *
    * @param segments
    *   The list of [[urldsl.vocabulary.Segment]] to match this path segment again.
    * @return
    */
  def matchFullSegments(segments: List[Segment]): Either[A, T] = for {
    matchOuptput <- matchSegments(segments)
    t <- matchOuptput.unusedSegments match {
      case Nil      => Right(matchOuptput.output)
      case segments => Left(errorImpl.endOfSegmentRequired(segments))
    }
  } yield t

  /** Matches the given raw `url` using the given [[urldsl.url.UrlStringParserGenerator]] for creating a
    * [[urldsl.url.UrlStringParser]].
    *
    * This method doesn't return the information about the remaining unused segments. The thought leading to this is
    * that [[urldsl.vocabulary.PathMatchOutput]] are supposed to be internal mechanics, while this method is supposed to
    * be the exposed interface of this [[urldsl.language.PathSegment]].
    *
    * @param url
    *   the url to parse. It has to be a well formed URL, otherwise this could raise an exception, depending on the
    *   provided [[urldsl.url.UrlStringParserGenerator]].
    * @param urlStringParserGenerator
    *   the [[urldsl.url.UrlStringParserGenerator]] used to create the [[urldsl.url.UrlStringParser]] that will actually
    *   parse the url to create the segments. The default one is usually a good choice. It has different implementations
    *   in JVM and JS, but they *should* behave the same way.
    * @return
    *   the output contained in the url, or the error if something fails.
    */
  def matchRawUrl(
      url: String,
      urlStringParserGenerator: UrlStringParserGenerator = UrlStringParserGenerator.defaultUrlStringParserGenerator
  ): Either[A, T] =
    matchFullSegments(urlStringParserGenerator.parser(url).segments)

  def matchPath(path: String, decoder: UrlStringDecoder = UrlStringDecoder.defaultDecoder): Either[A, T] =
    matchFullSegments(decoder.decodePath(path))

  /** Generate a list of segments representing the argument `t`.
    *
    * `matchSegments` and `createSegments` should be (functional) inverse of each other. That is,
    * `this.matchSegments(this.createSegments(t)) == Right(PathMathOutput(t, Nil))`
    */
  def createSegments(t: T): List[Segment]

  /** Sugar when `T =:= Unit`
    */
  final def createSegments()(implicit ev: Unit =:= T): List[Segment] = createSegments(ev(()))

  /** Concatenates the segments generated by `createSegments`
    */
  def createPath(t: T, encoder: UrlStringGenerator = UrlStringGenerator.default): String =
    encoder.makePath(createSegments(t))

  /** Sugar when `T =:= Unit`
    */
  final def createPath()(implicit ev: Unit =:= T): String =
    createPath(())
  final def createPath(encoder: UrlStringGenerator)(implicit ev: Unit =:= T): String =
    createPath((), encoder)

  final def createPart(t: T, encoder: UrlStringGenerator): String = createPath(t, encoder)

  /** Concatenates `this` [[urldsl.language.PathSegment]] with `that` one, "tupling" the types with the [[Composition]]
    * rules.
    */
  final def /[U](that: PathSegment[U, A])(implicit c: Composition[T, U]): PathSegment[c.Composed, A] =
    PathSegment.factory[c.Composed, A](
      (segments: List[Segment]) =>
        for {
          firstOut <- this.matchSegments(segments)
          PathMatchOutput(t, remaining) = firstOut
          secondOut <- that.matchSegments(remaining)
          PathMatchOutput(u, lastRemaining) = secondOut
        } yield PathMatchOutput(c.compose(t, u), lastRemaining),
      (out: c.Composed) => {
        val (t, u) = c.decompose(out)

        this.createSegments(t) ++ that.createSegments(u)
      }
    )

  final def ?[ParamsType, QPError](
      params: QueryParameters[ParamsType, QPError]
  ): PathSegmentWithQueryParams[T, A, ParamsType, QPError] =
    new PathSegmentWithQueryParams(this, params)

  /** Adds an extra satisfying criteria to the de-serialized output of this [[urldsl.language.PathSegment]].
    *
    * The new de-serialization works as follows:
    *   - if the initial de-serialization fails, then it returns the generated error
    *   - otherwise, if the de-serialized element satisfies the predicate, then it returns the element
    *   - if the predicate is false, generates the given `error` by feeding it the segments that it tried to match.
    *
    * This can be useful in, among others, two scenarios:
    *   - enforce bigger restriction on a segment (e.g., from integers to positive integer, regex match...)
    *   - in a multi-part segment, ensure consistency between the different component (e.g., a range of two integers
    *     that should not be too large...)
    */
  final def filter(predicate: T => Boolean, error: List[Segment] => A): PathSegment[T, A] =
    PathSegment.factory[T, A](
      (segments: List[Segment]) =>
        matchSegments(segments)
          .filterOrElse(((_: PathMatchOutput[T]).output).andThen(predicate), error(segments)),
      createSegments
    )

  /** Sugar for when `A =:= DummyError` */
  final def filter(predicate: T => Boolean)(implicit ev: A =:= DummyError): PathSegment[T, DummyError] = {
//    type F[+E] = PathSegment[T, E]
//    ev.liftCo[F].apply(this).filter(predicate, _ => DummyError.dummyError)
    // we keep the ugliness below while supporting 2.12 todo[scala3] remove this
    this.asInstanceOf[PathSegment[T, DummyError]].filter(predicate, _ => DummyError.dummyError)
  }

  /** Builds a [[PathSegment]] that first tries to match with this one, then tries to match with `that` one. If both
    * fail, the error of the second is returned (todo[behaviour]: should that change?)
    */
  final def ||[U](that: PathSegment[U, A]): PathSegment[Either[T, U], A] =
    PathSegment.factory[Either[T, U], A](
      segments =>
        this.matchSegments(segments) match {
          case Right(output) => Right(PathMatchOutput(Left(output.output), output.unusedSegments))
          case Left(_) =>
            that.matchSegments(segments).map(output => PathMatchOutput(Right(output.output), output.unusedSegments))
        },
      _.fold(this.createSegments, that.createSegments)
    )

  /** Casts this [[PathSegment]] to the new type U. Note that the [[urldsl.vocabulary.Codec]] must be an exception-free
    * bijection between T and U (or at least an embedding, if you know that you are doing).
    */
  final def as[U](implicit codec: Codec[T, U]): PathSegment[U, A] = as[U](codec.leftToRight _, codec.rightToLeft _)

  /** Casts this [[PathSegment]] to the new type U. The conversion functions should form an exception-free bijection
    * between T and U (or at least an embedding, if you know that you are doing).
    */
  final def as[U](fromTToU: T => U, fromUToT: U => T): PathSegment[U, A] = PathSegment.factory[U, A](
    (matchSegments _).andThen(_.map(_.map(fromTToU))),
    fromUToT.andThen(createSegments)
  )

  /** Matches using this [[PathSegment]], and then forgets its content. Uses the `default` value when creating the path
    * to go back.
    */
  final def ignore(default: => T): PathSegment[Unit, A] = PathSegment.factory[Unit, A](
    matchSegments(_).map(_.map(_ => ())),
    (_: Unit) => createSegments(default)
  )

  /** Forgets the information contained in the path parameter by injecting one. This turn this "dynamic" [[PathSegment]]
    * into a fix one.
    */
  final def provide(
      t: T
  )(implicit printer: Printer[T]): PathSegment[Unit, A] =
    PathSegment.factory[Unit, A](
      segments =>
        for {
          tMatch <- matchSegments(segments)
          PathMatchOutput(tOutput, unusedSegments) = tMatch
          unitMatched <-
            if (tOutput != t) Left(errorImpl.wrongValue(printer(t), printer(tOutput)))
            else Right(PathMatchOutput((), unusedSegments))
        } yield unitMatched,
      (_: Unit) => createSegments(t)
    )

  /** Associates this [[PathSegment]] with the given [[Fragment]] in order to match raw urls satisfying both conditions,
    * and returning the outputs from both.
    *
    * The query part of the url will be *ignored* (and will return Unit).
    */
  final def withFragment[FragmentType, FragmentError](
      fragment: Fragment[FragmentType, FragmentError]
  ): PathQueryFragmentRepr[T, A, Unit, Nothing, FragmentType, FragmentError] =
    new PathQueryFragmentRepr[T, A, Unit, Nothing, FragmentType, FragmentError](
      this,
      QueryParameters.ignore,
      fragment
    )

}

object PathSegment {

  type PathSegmentSimpleError[T] = PathSegment[T, SimplePathMatchingError]

  /** A Type of path segment where we don't care about the error.
    */
  type PathSegmentNoError[T] = PathSegment[T, DummyError]

  /** Trait factory */
  def factory[T, A](
      matching: List[Segment] => Either[A, PathMatchOutput[T]],
      creating: T => List[Segment]
  )(implicit errors: PathMatchingError[A]): PathSegment[T, A] = new PathSegment[T, A] {
    protected def errorImpl: PathMatchingError[A] = errors

    def matchSegments(segments: List[Segment]): Either[A, PathMatchOutput[T]] = matching(segments)

    def createSegments(t: T): List[Segment] = creating(t)
  }

  /** Simple path segment that matches everything by passing segments down the line. */
  final def empty[A](implicit pathMatchingError: PathMatchingError[A]): PathSegment[Unit, A] =
    factory[Unit, A](segments => Right(PathMatchOutput((), segments)), _ => Nil)
  final def root[A](implicit pathMatchingError: PathMatchingError[A]): PathSegment[Unit, A] = empty

  /** Simple path segment that matches nothing. This is the neutral of the || operator. */
  final def noMatch[A](implicit pathMatchingError: PathMatchingError[A]): PathSegment[Unit, A] =
    factory[Unit, A](_ => Left(pathMatchingError.unit), _ => Nil)

  /** Simple trait factory for "single segment"-oriented path Segments.
    *
    * This can be used to match a simple String, or a simple Int, etc...
    */
  final def simplePathSegment[T, A](matching: Segment => Either[A, T], creating: T => Segment)(implicit
      pathMatchingError: PathMatchingError[A]
  ): PathSegment[T, A] =
    factory(
      (_: Seq[Segment]) match {
        case Nil           => Left(pathMatchingError.missingSegment)
        case first :: rest => matching(first).map(PathMatchOutput(_, rest))
      },
      List(_).map(creating)
    )

  /** Matches a simple String and returning it. */
  final def stringSegment[A](implicit pathMatchingError: PathMatchingError[A]): PathSegment[String, A] =
    segment[String, A]

  /** Matches a simple Int and tries to convert it to an Int. */
  final def intSegment[A](implicit
      pathMatchingError: PathMatchingError[A],
      fromThrowable: ErrorFromThrowable[A]
  ): PathSegment[Int, A] = segment[Int, A]

  /** Creates a segment matching any element of type `T`, as long as the [[urldsl.vocabulary.FromString]] can
    * de-serialize it.
    */
  final def segment[T, A](implicit
      fromString: FromString[T, A],
      printer: Printer[T],
      error: PathMatchingError[A]
  ): PathSegment[T, A] = simplePathSegment[T, A]((fromString.apply _).compose(_.content), printer.print)

  /** Check that the segments ends at this point.
    */
  final def endOfSegments[A](implicit pathMatchingError: PathMatchingError[A]): PathSegment[Unit, A] = factory[Unit, A](
    (_: List[Segment]) match {
      case Nil => Right(PathMatchOutput((), Nil))
      case ss  => Left(pathMatchingError.endOfSegmentRequired(ss))
    },
    _ => Nil
  )

  /** Consumes all the remaining segments.
    *
    * This can be useful for static resources.
    */
  final def remainingSegments[A](implicit pathMatchingError: PathMatchingError[A]): PathSegment[List[String], A] =
    factory[List[String], A](
      segments => Right(PathMatchOutput(segments.map(_.content), Nil)),
      _.map(Segment.apply)
    )

  /** [[PathSegment]] that matches one of the given different possibilities.
    *
    * This can be useful in a Router, when you want to delegate the final decision to an inner router. Since all
    * possibilities are good, the creation of segment simply takes the first one.
    */
  final def oneOf[T, A](t: T, ts: T*)(implicit
      fromString: FromString[T, A],
      printer: Printer[T],
      pathMatchingError: PathMatchingError[A]
  ): PathSegment[Unit, A] = {
    val allTs = t +: ts.toList
    simplePathSegment(
      s =>
        fromString(s.content)
          .filterOrElse(
            allTs.contains,
            pathMatchingError.wrongValue("One of: " + allTs.map(printer.apply).mkString(", "), s.content)
          )
          .map(_ => ()),
      (_: Unit) => t
    )
  }

  /** Returns a [[urldsl.language.PathSegment]] which matches exactly the argument `t`.
    *
    * This conversion is implicit if you can provide a [[urldsl.vocabulary.FromString]] and a
    * [[urldsl.vocabulary.Printer]], so that it enables writing, e.g., `root / "hello" / true`
    */
  implicit final def unaryPathSegment[T, A](
      t: T
  )(implicit
      fromString: FromString[T, A],
      printer: Printer[T],
      pathMatchingError: PathMatchingError[A]
  ): PathSegment[Unit, A] =
    simplePathSegment(
      s =>
        fromString(s.content)
          .filterOrElse[A](_ == t, pathMatchingError.wrongValue(printer(t), s.content))
          .map(_ => ()),
      (_: Unit) => Segment(printer(t))
    )

  final lazy val dummyErrorImpl = PathSegmentImpl[DummyError]
  final lazy val simplePathErrorImpl = PathSegmentImpl[SimplePathMatchingError]

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy