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

zio.http.codec.PathCodec.scala Maven / Gradle / Ivy

There is a newer version: 3.0.1
Show newest version
/*
 * Copyright 2021 - 2023 Sporta Technologies PVT LTD & the ZIO HTTP contributors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package zio.http.codec

import scala.annotation.tailrec
import scala.collection.immutable.ListMap
import scala.collection.mutable
import scala.language.implicitConversions

import zio._

import zio.http._

/**
 * A codec for paths, which consists of segments, where each segment may be a
 * literal, an integer, a long, a string, a UUID, or the trailing path.
 *
 * {{{
 * import zio.http.endpoint.PathCodec._
 *
 * val pathCodec = empty / "users" / int("user-id") / "posts" / string("post-id")
 * }}}
 */
sealed trait PathCodec[A] extends codec.PathCodecPlatformSpecific { self =>
  import PathCodec._

  /**
   * Attaches documentation to the path codec, which may be used when generating
   * developer docs for a route.
   */
  def ??(doc: Doc): PathCodec[A] =
    self.annotate(MetaData.Documented(doc))

  final def ++[B](that: PathCodec[B])(implicit combiner: Combiner[A, B]): PathCodec[combiner.Out] =
    PathCodec.Concat(self, that, combiner)

  final def /[B](that: PathCodec[B])(implicit combiner: Combiner[A, B]): PathCodec[combiner.Out] =
    self ++ that

  final def /[Env, Err](routes: Routes[Env, Err])(implicit
    ev: PathCodec[A] <:< PathCodec[Unit],
  ): Routes[Env, Err] =
    routes.nest(ev(self))

  final def annotate(metaData: MetaData[A]): PathCodec[A] = {
    self match {
      case Annotated(codc, annotations) => Annotated(codc, annotations :+ metaData)
      case _                            => Annotated(self, Chunk(metaData))
    }
  }

  private[http] def orElse(value: PathCodec[Unit])(implicit ev: A =:= Unit): PathCodec[Unit] =
    Fallback(self.asInstanceOf[PathCodec[Unit]], value)

  private def fallbackAlternatives(f: Fallback[_]): List[PathCodec[Any]] = {
    @tailrec
    def loop(codecs: List[PathCodec[_]], result: List[PathCodec[_]]): List[PathCodec[_]] =
      if (codecs.isEmpty) result
      else
        codecs.head match {
          case PathCodec.Annotated(codec, _)              =>
            loop(codec :: codecs.tail, result)
          case PathCodec.Segment(SegmentCodec.Literal(_)) =>
            loop(codecs.tail, result :+ codecs.head)
          case PathCodec.Segment(SegmentCodec.Empty)      =>
            loop(codecs.tail, result)
          case Fallback(left, right)                      =>
            loop(left :: right :: codecs.tail, result)
          case other                                      =>
            throw new IllegalStateException(s"Alternative path segments should only contain literals, found: $other")
        }
    loop(List(f.left, f.right), List.empty).asInstanceOf[List[PathCodec[Any]]]
  }

  final def alternatives: List[PathCodec[A]] = {
    var alts                                                      = List.empty[PathCodec[Any]]
    def loop(codec: PathCodec[_], combiner: Combiner[_, _]): Unit = codec match {
      case Concat(left, right, combiner) =>
        loop(left, combiner)
        loop(right, combiner)
      case f: Fallback[_]                =>
        if (alts.isEmpty) alts = fallbackAlternatives(f)
        else
          alts ++= alts.flatMap { alt =>
            fallbackAlternatives(f).map(fa =>
              Concat(alt, fa.asInstanceOf[PathCodec[Any]], combiner.asInstanceOf[Combiner.WithOut[Any, Any, Any]]),
            )
          }
      case Segment(SegmentCodec.Empty)   =>
        alts :+= codec.asInstanceOf[PathCodec[Any]]
      case pc                            =>
        if (alts.isEmpty) alts :+= pc.asInstanceOf[PathCodec[Any]]
        else
          alts = alts
            .map(l =>
              Concat(l, pc.asInstanceOf[PathCodec[Any]], combiner.asInstanceOf[Combiner.WithOut[Any, Any, Any]])
                .asInstanceOf[PathCodec[Any]],
            )
    }
    loop(self, Combiner.leftUnit[Unit])
    alts.asInstanceOf[List[PathCodec[A]]]
  }

  final def asType[B](implicit ev: A =:= B): PathCodec[B] = self.asInstanceOf[PathCodec[B]]

  /**
   * Decodes a method and path into a value of type `A`.
   */
  final def decode(path: Path): Either[String, A] = {
    import PathCodec.Opt._

    val instructions = optimize
    val segments     = path.segments

    var i                           = 0
    var j                           = 0
    var fail                        = ""
    val stack: java.util.Deque[Any] = new java.util.ArrayDeque[Any](2)

    // For root:
    stack.push(())

    while (i < instructions.length) {
      val opt = instructions(i)

      opt match {
        case Match(value)     =>
          if (j >= segments.length || segments(j) != value) {
            fail = "Expected path segment \"" + value + "\" but found end of path"
            i = instructions.length
          } else {
            stack.push(())
            j = j + 1
          }
        case MatchAny(values) =>
          if (j >= segments.length || !values.contains(segments(j))) {
            fail = "Expected one of the following path segments: " + values.mkString(", ") + " but found end of path"
            i = instructions.length
          } else {
            stack.push(())
            j = j + 1
          }

        case Combine(combiner0) =>
          val combiner = combiner0.asInstanceOf[Combiner[Any, Any]]
          val right    = stack.pop()
          val left     = stack.pop()
          stack.push(combiner.combine(left, right))

        case IntOpt =>
          if (j >= segments.length) {
            fail = "Expected integer path segment but found end of path"
            i = instructions.length
          } else {
            val segment = segments(j)
            j = j + 1
            try {
              stack.push(segment.toInt)
            } catch {
              case _: NumberFormatException =>
                fail = "Expected integer path segment but found \"" + segment + "\""
                i = instructions.length
            }
          }

        case LongOpt   =>
          if (j >= segments.length) {
            fail = "Expected long path segment but found end of path"
            i = instructions.length
          } else {
            val segment = segments(j)
            j = j + 1
            try {
              stack.push(segment.toLong)
            } catch {
              case _: NumberFormatException =>
                fail = s"Expected long path segment but found ${segment}"
                i = instructions.length
            }
          }
        case StringOpt =>
          if (j >= segments.length) {
            fail = "Expected text path segment but found end of path"
            i = instructions.length
          } else {
            val segment = segments(j)
            j = j + 1
            stack.push(segment)
          }

        case UUIDOpt =>
          if (j >= segments.length) {
            fail = "Expected UUID path segment but found end of path"
            i = instructions.length
          } else {
            val segment = segments(j)
            j = j + 1
            try {
              stack.push(java.util.UUID.fromString(segment))
            } catch {
              case _: IllegalArgumentException =>
                fail = s"Expected UUID path segment but found ${segment}"
                i = instructions.length
            }
          }

        case BoolOpt =>
          if (j >= segments.length) {
            fail = "Expected boolean path segment but found end of path"
            i = instructions.length
          } else {
            val segment = segments(j)
            j = j + 1

            if (segment.equalsIgnoreCase("true")) {
              stack.push(true)
            } else if (segment.equalsIgnoreCase("false")) {
              stack.push(false)
            } else {
              fail = s"Expected boolean path segment but found ${segment}"
              i = instructions.length
            }
          }

        case TrailingOpt =>
          // Consume all Trailing, possibly empty:
          if (j >= segments.length) {
            val result =
              if (path.hasTrailingSlash) Path.root else Path.empty

            stack.push(result)
          } else {
            val flags =
              if (j == 0) path.flags
              else if (path.hasTrailingSlash) Path.Flags(Path.Flag.TrailingSlash)
              else 0

            stack.push(Path(flags, segments.drop(j)))
            j = segments.length
          }

        case Unit =>
          stack.push(())

        case MapOrFail(f) =>
          f(stack.pop) match {
            case Left(failure) =>
              fail = failure
              i = instructions.length
            case Right(value)  =>
              stack.push(value)
          }

        case SubSegmentOpts(ops) =>
          val error = decodeSubstring(segments(j), ops, stack)
          if (error != null) {
            fail = error
            i = instructions.length
          } else {
            j += 1
          }
      }

      i = i + 1
    }
    if (fail != "") Left(fail)
    else {
      if (j < segments.length) {
        val rest = segments.drop(j).mkString("/")
        Left(s"Expected end of path but found: ${rest}")
      } else {
        Right(stack.pop().asInstanceOf[A])
      }
    }
  }

  private def decodeSubstring(
    value: String,
    instructions: Array[Opt],
    stack: java.util.Deque[Any],
  ): String = {
    import Opt._

    var i    = 0
    var j    = 0
    val size = value.length
    while (i < instructions.length) {
      val opt = instructions(i)
      opt match {
        case Match(toMatch)     =>
          val size0 = toMatch.length
          if ((size - j) < size0) {
            return "Expected \"" + toMatch + "\" in segment " + value + " but found end of segment"
          } else if (value.startsWith(toMatch, j)) {
            stack.push(())
            j += size0
          } else {
            return "Expected \"" + toMatch + "\" in segment " + value + " but found: " + value.substring(j)
          }
        case Combine(combiner0) =>
          val combiner = combiner0.asInstanceOf[Combiner[Any, Any]]
          val right    = stack.pop()
          val left     = stack.pop()
          stack.push(combiner.combine(left, right))
        case StringOpt          =>
          // Here things get "interesting" (aka annoying). We don't have a way of knowing when a string ends,
          // so we have to look ahead to the next operator and figure out where it begins
          val end = indexOfNextCodec(value, instructions, i, j)
          if (end == -1) { // If this wasn't the last codec, let the error handler of the next codec handle this
            stack.push(value.substring(j))
            j = size
          } else {
            stack.push(value.substring(j, end))
            j = end
          }
        case IntOpt             =>
          val isNegative = value(j) == '-'
          if (isNegative) j += 1
          var end        = j
          while (end < size && value(end).isDigit) end += 1
          if (end == j) {
            return "Expected integer path segment but found end of segment"
          } else if (end - j > 10) {
            return "Expected integer path segment but found: " + value.substring(j, end)
          } else {

            try {
              val int = parseInt(value, j, end, 10)
              j = end
              if (isNegative) stack.push(-int) else stack.push(int)
            } catch {
              case _: NumberFormatException =>
                return "Expected integer path segment but found: " + value.substring(j, end)
            }
          }
        case LongOpt            =>
          val isNegative = value(j) == '-'
          if (isNegative) j += 1
          var end        = j
          while (end < size && value(end).isDigit) end += 1
          if (end == j) {
            return "Expected long path segment but found end of segment"
          } else if (end - j > 19) {
            return "Expected long path segment but found: " + value.substring(j, end)
          } else {
            try {
              val long = parseLong(value, j, end, 10)
              j = end
              if (isNegative) stack.push(-long) else stack.push(long)
            } catch {
              case _: NumberFormatException => return "Expected long path segment but found: " + value.substring(j, end)
            }
          }
        case UUIDOpt            =>
          if ((size - j) < 36) {
            return "Remaining path segment " + value.substring(j) + " is too short to be a UUID"
          } else {
            val sub = value.substring(j, j + 36)
            try {
              stack.push(java.util.UUID.fromString(sub))
            } catch {
              case _: IllegalArgumentException => return "Expected UUID path segment but found: " + sub
            }
            j += 36
          }
        case BoolOpt            =>
          if (value.regionMatches(true, j, "true", 0, 4)) {
            stack.push(true)
            j += 4
          } else if (value.regionMatches(true, j, "false", 0, 5)) {
            stack.push(false)
            j += 5
          } else {
            return "Expected boolean path segment but found end of segment"
          }
        case TrailingOpt        =>
          // TrailingOpt must be invalid, since it wants to extract a path,
          // which is not possible in a sub part of a segment.
          // The equivalent of trailing here is just StringOpt
          throw new IllegalStateException("TrailingOpt is not allowed in a sub segment")
        case _                  =>
          throw new IllegalStateException("Unexpected instruction in substring decoder")
      }
      i += 1
    }
    if (j != size) "Expected end of segment but found: " + value.substring(j)
    else null
  }

  private def indexOfNextCodec(value: String, instructions: Array[Opt], fromI: Int, idx: Int): Int = {
    import Opt._

    var nextOpt = null.asInstanceOf[Opt]
    var j1      = fromI + 1

    while ((nextOpt eq null) && j1 < instructions.length) {
      instructions(j1) match {
        case op @ (Match(_) | IntOpt | LongOpt | UUIDOpt | BoolOpt) =>
          nextOpt = op
        case _                                                      =>
          j1 += 1
      }
    }

    nextOpt match {
      case null             =>
        -1
      case Match(toMatch)   =>
        if (idx + toMatch.length > value.length) -1
        else if (toMatch.length == 1) value.indexOf(toMatch.charAt(0).toInt, idx)
        else value.indexOf(toMatch, idx)
      case IntOpt | LongOpt =>
        value.indexWhere(_.isDigit, idx)
      case BoolOpt          =>
        val t = value.regionMatches(true, idx, "true", 0, 4)
        if (t) idx + 4 else if (value.regionMatches(true, idx, "false", 0, 5)) idx + 5 else -1
      case UUIDOpt          =>
        val until = SegmentCodec.UUID.inUUIDUntil(value, idx)
        if (until == -1) -1 else idx + until
      case MatchAny(values) =>
        var end      = -1
        val valuesIt = values.iterator
        while (valuesIt.hasNext && end == -1) {
          val value = valuesIt.next()
          val index = value.indexOf(value, idx)
          if (index != -1) end = index
        }
        end
      case _                =>
        throw new IllegalStateException("Unexpected instruction in substring decoder: " + nextOpt)
    }
  }

  /**
   * Returns the documentation for the path codec, if any.
   */
  def doc: Doc =
    self match {
      case Segment(_)                    => Doc.empty
      case TransformOrFail(api, _, _)    => api.doc
      case Concat(left, right, _)        => left.doc + right.doc
      case Annotated(codec, annotations) =>
        codec.doc + annotations.collectFirst { case MetaData.Documented(doc) => doc }.getOrElse(Doc.empty)
      case Fallback(left, right)         => left.doc + right.doc
    }

  /**
   * Encodes a value of type `A` into the method and path that this route
   * pattern would successfully match against.
   */
  final def encode(value: A): Either[String, Path] = format(value)

  private[http] final def erase: PathCodec[Any] = self.asInstanceOf[PathCodec[Any]]

  final def example(name: String, example: A): PathCodec[A] =
    annotate(MetaData.Examples(Map(name -> example)))

  final def examples(examples: (String, A)*): PathCodec[A] =
    annotate(MetaData.Examples(examples.toMap))

  /**
   * Formats a value of type `A` into a path. This is useful for embedding paths
   * into HTML that is rendered by the server.
   */
  final def format(value: A): Either[String, Path] = {
    def loop(path: PathCodec[_], value: Any): Either[String, Path] = path match {
      case PathCodec.Annotated(codec, _)           =>
        loop(codec, value)
      case PathCodec.Concat(left, right, combiner) =>
        val (leftValue, rightValue) = combiner.separate(value.asInstanceOf[combiner.Out])

        for {
          leftPath  <- loop(left, leftValue)
          rightPath <- loop(right, rightValue)
        } yield leftPath ++ rightPath

      case PathCodec.Segment(segment) =>
        Right(segment.format(value.asInstanceOf[segment.Type]))

      case PathCodec.TransformOrFail(api, _, g) =>
        g.asInstanceOf[Any => Either[String, Any]](value).flatMap(loop(api, _))
      case Fallback(left, _)                    =>
        loop(left, value)
    }

    loop(self, value).map { path =>
      if (path.nonEmpty) path.addLeadingSlash else path
    }
  }

  /**
   * Determines if this pattern matches the specified method and path. Rather
   * than use this method, you should just try to decode it directly, for higher
   * performance, otherwise the same information will be decoded twice.
   */
  final def matches(path: Path): Boolean =
    decode(path).isRight

  private var _optimize: Array[Opt] = null.asInstanceOf[Array[Opt]]

  private[http] def optimize: Array[Opt] = {

    def loopSegment(segment: SegmentCodec[_], fresh: Boolean)(implicit b: mutable.ArrayBuilder[Opt]): Unit =
      segment match {
        case SegmentCodec.Empty                           => b += Opt.Unit
        case SegmentCodec.Literal(value)                  => b += Opt.Match(value)
        case SegmentCodec.IntSeg(_)                       => b += Opt.IntOpt
        case SegmentCodec.LongSeg(_)                      => b += Opt.LongOpt
        case SegmentCodec.Text(_)                         => b += Opt.StringOpt
        case SegmentCodec.UUID(_)                         => b += Opt.UUIDOpt
        case SegmentCodec.BoolSeg(_)                      => b += Opt.BoolOpt
        case SegmentCodec.Trailing                        => b += Opt.TrailingOpt
        case SegmentCodec.Combined(left, right, combiner) =>
          val ab = if (fresh) mutable.ArrayBuilder.make[Opt] else b
          loopSegment(left, fresh = false)(ab)
          loopSegment(right, fresh = false)(ab)
          ab += Opt.Combine(combiner)
          if (fresh) b += Opt.SubSegmentOpts(ab.result().asInstanceOf[Array[Opt]])
      }

    def loop(pattern: PathCodec[_])(implicit b: mutable.ArrayBuilder[Opt]): Unit =
      pattern match {
        case PathCodec.Annotated(codec, _) =>
          loop(codec)
        case PathCodec.Segment(segment)    =>
          loopSegment(segment, fresh = true)
        case f: Fallback[_]                =>
          b += Opt.MatchAny(fallbacks(f))
        case Concat(left, right, combiner) =>
          loop(left)
          loop(right)
          b += Opt.Combine(combiner)
        case TransformOrFail(api, f, _)    =>
          loop(api)
          b += Opt.MapOrFail(f.asInstanceOf[Any => Either[String, Any]])
      }

    if (_optimize eq null) {
      val b: mutable.ArrayBuilder[Opt] = mutable.ArrayBuilder.make[Opt]
      loop(self)(b)
      _optimize = b.result()
    }

    _optimize
  }

  private def fallbacks(f: Fallback[_]): Set[String] = {
    @tailrec
    def loop(codecs: List[PathCodec[_]], result: Set[String]): Set[String] =
      if (codecs.isEmpty) result
      else
        codecs.head match {
          case PathCodec.Annotated(codec, _)                  =>
            loop(codec :: codecs.tail, result)
          case PathCodec.Segment(SegmentCodec.Literal(value)) =>
            loop(codecs.tail, result + value)
          case PathCodec.Segment(SegmentCodec.Empty)          =>
            loop(codecs.tail, result)
          case Fallback(left, right)                          =>
            loop(left :: right :: codecs.tail, result)
          case other                                          =>
            throw new IllegalStateException(s"Alternative path segments should only contain literals, found: $other")
        }
    loop(List(f.left, f.right), Set.empty)
  }

  /**
   * Renders the path codec as a string.
   */
  def render: String =
    render("{", "}")

  /**
   * Renders the path codec as a string. Surrounds the path variables with the
   * specified prefix and suffix.
   */
  def render(prefix: String, suffix: String): String = {
    def loop(path: PathCodec[_]): String = path match {
      case PathCodec.Annotated(codec, _)        =>
        loop(codec)
      case PathCodec.Concat(left, right, _)     =>
        loop(left) + loop(right)
      case PathCodec.Segment(segment)           =>
        segment.render(prefix, suffix)
      case PathCodec.TransformOrFail(api, _, _) =>
        loop(api)
      case PathCodec.Fallback(left, _)          =>
        loop(left)
    }

    loop(self)
  }

  private[zio] def renderIgnoreTrailing: String =
    renderIgnoreTrailing("{", "}")

  private[zio] def renderIgnoreTrailing(prefix: String, suffix: String): String = {
    def loop(path: PathCodec[_]): String = path match {
      case PathCodec.Annotated(codec, _)    =>
        loop(codec)
      case PathCodec.Concat(left, right, _) =>
        loop(left) + loop(right)

      case PathCodec.Segment(SegmentCodec.Trailing) => ""

      case PathCodec.Segment(segment) => segment.render(prefix, suffix)

      case PathCodec.TransformOrFail(api, _, _) => loop(api)

      case PathCodec.Fallback(left, _) => loop(left)
    }

    loop(self)
  }

  /**
   * Returns the segments of the path codec.
   */
  def segments: Chunk[SegmentCodec[_]] = {
    def loop(path: PathCodec[_]): Chunk[SegmentCodec[_]] = path match {
      case PathCodec.Annotated(codec, _) =>
        loop(codec)
      case PathCodec.Segment(segment)    => Chunk(segment)

      case PathCodec.Concat(left, right, _) =>
        loop(left) ++ loop(right)

      case PathCodec.TransformOrFail(api, _, _) =>
        loop(api)

      case PathCodec.Fallback(left, _) =>
        loop(left)
    }

    loop(self)
  }

  override def toString: String = render

  final def transform[A2](f: A => A2)(g: A2 => A): PathCodec[A2] =
    PathCodec.TransformOrFail[A, A2](self, in => Right(f(in)), output => Right(g(output)))

  final def transformOrFail[A2](f: A => Either[String, A2])(g: A2 => Either[String, A]): PathCodec[A2] =
    PathCodec.TransformOrFail[A, A2](self, f, g)

  final def transformOrFailLeft[A2](f: A => Either[String, A2])(g: A2 => A): PathCodec[A2] =
    PathCodec.TransformOrFail[A, A2](self, f, output => Right(g(output)))

  final def transformOrFailRight[A2](f: A => A2)(g: A2 => Either[String, A]): PathCodec[A2] =
    PathCodec.TransformOrFail[A, A2](self, in => Right(f(in)), g)
}
object PathCodec {

  /**
   * Constructs a path codec from a method and a path literal.
   */
  def apply(value: String): PathCodec[Unit] = {
    val path = Path(value)

    (path.segments: @unchecked) match {
      case Chunk()                 => PathCodec.empty
      case Chunk(first, rest @ _*) =>
        rest.foldLeft[PathCodec[Unit]](Segment(SegmentCodec.literal(first))) { (pathSpec, segment) =>
          pathSpec / Segment(SegmentCodec.literal(segment))
        }
    }

  }

  def bool(name: String): PathCodec[Boolean] = Segment(SegmentCodec.bool(name))

  /**
   * The empty / root path codec.
   */
  def empty: PathCodec[Unit] = Segment[Unit](SegmentCodec.Empty)

  def int(name: String): PathCodec[Int] = Segment(SegmentCodec.int(name))

  def literal(value: String): PathCodec[Unit] = apply(value)

  def long(name: String): PathCodec[Long] = Segment(SegmentCodec.long(name))

  implicit def path(value: String): PathCodec[Unit] = apply(value)

  implicit def segment[A](codec: SegmentCodec[A]): PathCodec[A] = Segment(codec)

  def string(name: String): PathCodec[String] = Segment(SegmentCodec.string(name))

  def trailing: PathCodec[Path] = Segment(SegmentCodec.Trailing)

  def uuid(name: String): PathCodec[java.util.UUID] = Segment(SegmentCodec.uuid(name))

  private[http] final case class Fallback[A](left: PathCodec[Unit], right: PathCodec[Unit]) extends PathCodec[A]

  private[http] final case class Segment[A](segment: SegmentCodec[A]) extends PathCodec[A]

  private[http] final case class Concat[A, B, C](
    left: PathCodec[A],
    right: PathCodec[B],
    combiner: Combiner.WithOut[A, B, C],
  ) extends PathCodec[C]

  private[http] final case class TransformOrFail[X, A](
    api: PathCodec[X],
    f: X => Either[String, A],
    g: A => Either[String, X],
  ) extends PathCodec[A] {
    type In  = X
    type Out = A
  }

  final case class Annotated[A](codec: PathCodec[A], annotations: Chunk[MetaData[A]]) extends PathCodec[A] {

    override def equals(that: Any): Boolean =
      codec.equals(that)

  }

  sealed trait MetaData[A] extends Product with Serializable

  object MetaData {
    final case class Documented[A](value: Doc)             extends MetaData[A]
    final case class Examples[A](examples: Map[String, A]) extends MetaData[A]
  }

  private[http] val someUnit = Some(())

  /**
   * An optimized representation of the process of decoding a path and producing
   * a value. This is built for an evaluator that uses a stack.
   */
  private[http] sealed trait Opt
  private[http] object Opt {
    final case class Match(value: String)                     extends Opt
    final case class MatchAny(values: Set[String])            extends Opt
    final case class Combine(combiner: Combiner[_, _])        extends Opt
    case object IntOpt                                        extends Opt
    case object LongOpt                                       extends Opt
    case object StringOpt                                     extends Opt
    case object UUIDOpt                                       extends Opt
    case object BoolOpt                                       extends Opt
    case object TrailingOpt                                   extends Opt
    case object Unit                                          extends Opt
    final case class SubSegmentOpts(ops: Array[Opt])          extends Opt
    final case class MapOrFail(f: Any => Either[String, Any]) extends Opt
  }

  private[http] final case class SegmentSubtree[+A](
    literals: ListMap[String, SegmentSubtree[A]],
    others: ListMap[SegmentCodec[_], SegmentSubtree[A]],
    literalsWithCollisions: Set[String],
    value: Chunk[A],
  ) {
    self =>
    def ++[A1 >: A](that: SegmentSubtree[A1]): SegmentSubtree[A1] = {
      val newLiterals          = mergeMaps(self.literals, that.literals)(_ ++ _)
      val newOthers            = mergeMaps(self.others, that.others)(_ ++ _)
      val newLiteralCollisions = mergeLiteralCollisions(
        self.literalsWithCollisions ++ that.literalsWithCollisions,
        newLiterals.keySet,
        newOthers.keys,
      )
      SegmentSubtree(
        newLiterals,
        newOthers,
        newLiteralCollisions,
        self.value ++ that.value,
      )
    }

    def add[A1 >: A](segments: Iterable[SegmentCodec[_]], value: A1): SegmentSubtree[A1] =
      self ++ SegmentSubtree.single(segments, value)

    def get(path: Path): Chunk[A] =
      get(path, 0)

    private def get(path: Path, from: Int, skipLiteralsFor: Set[Int] = Set.empty): Chunk[A] = {
      val segments  = path.segments
      val nSegments = segments.length
      var subtree   = self
      var result    = subtree.value
      var i         = from

      var trySkipLiteralIdx: List[Int] = Nil

      while (i < nSegments) {
        val segment = segments(i)

        // Fast path, jump down the tree:
        if (!skipLiteralsFor.contains(i) && subtree.literals.contains(segment)) {

          // this subtree segment have conflict with others
          // will try others if result was empty
          if (subtree.literalsWithCollisions.contains(segment)) {
            trySkipLiteralIdx = i +: trySkipLiteralIdx
          }

          subtree = subtree.literals(segment)

          result = subtree.value
          i += 1
        } else {
          val flattened = subtree.othersFlat

          subtree = null
          flattened.length match {
            case 0 => // No predicates to evaluate
            case 1 => // Only 1 predicate to evaluate (most common)
              val (codec, subtree0) = flattened(0)
              val matched           = codec.matches(segments, i)
              if (matched > 0) {
                subtree = subtree0
                result = subtree0.value
                i += matched
              }
            case n => // Slowest fallback path. Have to to find the first predicate where the subpath returns a result
              val matches         = Array.ofDim[Int](n)
              var index           = 0
              var nPositive       = 0
              var lastPositiveIdx = -1
              while (index < n) {
                val (codec, _) = flattened(index)
                val n          = codec.matches(segments, i)
                if (n > 0) {
                  matches(index) = n
                  nPositive += 1
                  lastPositiveIdx = index
                }
                index += 1
              }

              nPositive match {
                case 0 => ()
                case 1 =>
                  subtree = flattened(lastPositiveIdx)._2
                  result = subtree.value
                  i += matches(lastPositiveIdx)
                case _ =>
                  index = 0
                  while (index < n && (subtree eq null)) {
                    val matched = matches(index)
                    if (matched > 0) {
                      val (_, subtree0) = flattened(index)
                      if (subtree0.get(path, i + matched).nonEmpty) {
                        subtree = subtree0
                        result = subtree.value
                        i += matched
                      }
                    }
                    index += 1
                  }
              }
          }

          if (subtree eq null) {
            result = Chunk.empty
            i = nSegments
          }
        }
      }

      // Might be some other matches because trailing matches everything:
      if (subtree ne null) {
        subtree.others.get(SegmentCodec.trailing) match {
          case Some(subtree) =>
            result = result ++ subtree.value
          case None          =>
        }
      }

      if (trySkipLiteralIdx.nonEmpty && result.isEmpty) {
        trySkipLiteralIdx = trySkipLiteralIdx.reverse
        while (trySkipLiteralIdx.nonEmpty && result.isEmpty) {
          val skipIdx = trySkipLiteralIdx.head
          trySkipLiteralIdx = trySkipLiteralIdx.tail
          result = get(path, from, skipLiteralsFor + skipIdx)
        }
        result
      } else result
    }

    def map[B](f: A => B): SegmentSubtree[B] =
      SegmentSubtree(
        literals.map { case (k, v) => k -> v.map(f) },
        ListMap(others.toSeq.map { case (k, v) => k -> v.map(f) }: _*),
        literalsWithCollisions,
        value.map(f),
      )

    private var _othersFlat = null.asInstanceOf[Chunk[(SegmentCodec[_], SegmentSubtree[Any])]]

    private def othersFlat: Chunk[(SegmentCodec[_], SegmentSubtree[A])] = {
      if (_othersFlat eq null) _othersFlat = Chunk.fromIterable(others)
      _othersFlat.asInstanceOf[Chunk[(SegmentCodec[_], SegmentSubtree[A])]]
    }
  }
  object SegmentSubtree    {
    def single[A](segments: Iterable[SegmentCodec[_]], value: A): SegmentSubtree[A] =
      segments.collect { case x if x.nonEmpty => x }
        .foldRight[SegmentSubtree[A]](SegmentSubtree(ListMap(), ListMap(), Set.empty, Chunk(value))) {
          case (segment, subtree) =>
            val literals =
              segment match {
                case SegmentCodec.Literal(value) => ListMap(value -> subtree)
                case _                           => ListMap.empty[String, SegmentSubtree[A]]
              }

            val others =
              ListMap[SegmentCodec[_], SegmentSubtree[A]]((segment match {
                case SegmentCodec.Literal(_) => Chunk.empty
                case _                       => Chunk((segment, subtree))
              }): _*)

            SegmentSubtree(literals, others, Set.empty, Chunk.empty)
        }

    val empty: SegmentSubtree[Nothing] =
      SegmentSubtree(ListMap(), ListMap(), Set.empty, Chunk.empty)
  }

  private def mergeMaps[A, B](left: ListMap[A, B], right: ListMap[A, B])(f: (B, B) => B): ListMap[A, B] =
    right.foldLeft(left) { case (acc, (k, v)) =>
      acc.get(k) match {
        case None     => acc.updated(k, v)
        case Some(v0) => acc.updated(k, f(v0, v))
      }
    }

  private def mergeLiteralCollisions(
    currentCollisions: Set[String],
    literals: Set[String],
    others: Iterable[SegmentCodec[_]],
  ): Set[String] = {
    currentCollisions ++ literals.filter { literal =>
      !currentCollisions.contains(literal) && others.exists { o =>
        o.inSegmentUntil(literal, 0) != -1
      }
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy