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

caliban.rendering.Renderer.scala Maven / Gradle / Ivy

The newest version!
package caliban.rendering

import scala.annotation.switch

/**
 * The inverse of a `Parser` over some type A.
 * A renderer can be used to render a value of type A to a string in either a regular or compact format.
 *
 * For specializations actually relevant to graphql see [[caliban.rendering.ValueRenderer]] and [[caliban.rendering.DocumentRenderer]]
 */
trait Renderer[-A] { self =>
  def render(a: A): String = {
    val sb = new StringBuilder
    unsafeRender(a, Some(0), sb)
    sb.toString()
  }

  def renderCompact(a: A): String = {
    val sb = new StringBuilder
    unsafeRender(a, None, sb)
    sb.toString()
  }

  /**
   * Combines this renderer with another renderer sequentially. Semantically equivalent to `this andThen that`
   */
  def ++[A1 <: A](that: Renderer[A1]): Renderer[A1] = self match {
    case Renderer.Combined(renderers) => Renderer.Combined(renderers :+ that)
    case _                            => Renderer.Combined(List(self, that))
  }

  /**
   * Contramaps the input of this renderer with the given function producing a renderer that now operates on type B
   */
  def contramap[B](f: B => A): Renderer[B] = new Renderer[B] {
    override def unsafeRender(value: B, indent: Option[Int], write: StringBuilder): Unit =
      self.unsafeRender(f(value), indent, write)
  }

  /**
   * Returns an optional renderer that will only render the value if it is defined
   */
  def optional: Renderer[Option[A]] = new Renderer[Option[A]] {
    override def unsafeRender(value: Option[A], indent: Option[Int], write: StringBuilder): Unit =
      value.foreach(self.unsafeRender(_, indent, write))
  }

  /**
   * Returns a renderer that renders a list of A where the underlying renderer is responsible for rendering the
   * separator between each element.
   */
  def list: Renderer[List[A]] =
    list(Renderer.empty)

  /**
   * Returns a renderer that renders a list of A but where the separator is rendered by provided argument renderer.
   * The second parameter determines whether to print the separator before the first element or not.
   */
  def list[A1 <: A](separator: Renderer[A1], omitFirst: Boolean = true): Renderer[List[A1]] = new Renderer[List[A1]] {
    override protected[caliban] def unsafeRender(value: List[A1], indent: Option[Int], write: StringBuilder): Unit = {
      var first = omitFirst
      value.foreach { v =>
        if (first) first = false
        else separator.unsafeRender(v, indent, write)
        self.unsafeRender(v, indent, write)
      }
    }
  }

  /**
   * Returns a renderer that renders a set of A but where the separator is rendered by provided argument renderer.
   */
  def set[A1 <: A](separator: Renderer[A1]): Renderer[Set[A1]] = new Renderer[Set[A1]] {
    override protected[caliban] def unsafeRender(value: Set[A1], indent: Option[Int], write: StringBuilder): Unit = {
      var first = true
      value.foreach { v =>
        if (first) first = false
        else separator.unsafeRender(v, indent, write)
        self.unsafeRender(v, indent, write)
      }
    }
  }

  /**
   * Returns a renderer that will only render when the provided predicate is true.
   */
  def when[A1 <: A](pred: A1 => Boolean): Renderer[A1] = new Renderer[A1] {
    override protected[caliban] def unsafeRender(value: A1, indent: Option[Int], write: StringBuilder): Unit =
      if (pred(value)) self.unsafeRender(value, indent, write)
  }

  /**
   * Protected method for implementers to override. This method provides the actual unsafe rendering logic.
   * @param value the value to render
   * @param indent the current indentation level. This will be None if the renderer is rendering in compact mode or Some(n)
   *               if the renderer is rendering in regular mode where n is the current indentation level.
   * @param write the string builder to write to
   */
  protected[caliban] def unsafeRender(value: A, indent: Option[Int], write: StringBuilder): Unit
}

object Renderer {

  def combine[A](renderers: Renderer[A]*): Renderer[A] =
    Combined(renderers.toList)

  /**
   * A Renderer which always renders a single character.
   */
  def char(char: Char): Renderer[Any] = new Renderer[Any] {
    override def unsafeRender(value: Any, indent: Option[Int], write: StringBuilder): Unit =
      write.append(char)
  }

  def comma: Renderer[Any] = char(',')

  /**
   * A Renderer which always renders a string.
   */
  def string(str: String): Renderer[Any] = new Renderer[Any] {
    override def unsafeRender(value: Any, indent: Option[Int], write: StringBuilder): Unit =
      write.append(str)
  }

  /**
   * A Renderer which simply renders the input string
   */
  lazy val string: Renderer[String] = new Renderer[String] {
    override def unsafeRender(value: String, indent: Option[Int], write: StringBuilder): Unit =
      write.append(value)
  }

  lazy val escapedString: Renderer[String] = new Renderer[String] {
    override def unsafeRender(value: String, indent: Option[Int], write: StringBuilder): Unit =
      unsafeFastEscape(value, write)

    private def unsafeFastEscape(value: String, writer: StringBuilder): Unit = {
      var i = 0
      while (i < value.length) {
        (value.charAt(i): @switch) match {
          case '\\' => writer.append("\\\\")
          case '\b' => writer.append("\\b")
          case '\f' => writer.append("\\f")
          case '\n' => writer.append("\\n")
          case '\r' => writer.append("\\r")
          case '\t' => writer.append("\\t")
          case '"'  => writer.append("\\\"")
          case c    => writer.append(c)
        }
        i += 1
      }
    }
  }

  /**
   * A Renderer which doesn't render anything.
   */
  lazy val empty: Renderer[Any] = new Renderer[Any] {
    override def unsafeRender(value: Any, indent: Option[Int], write: StringBuilder): Unit = ()
  }

  /**
   * A Renderer which renders a space character when in non-compact mode.
   */
  lazy val spaceOrEmpty: Renderer[Any] = new Renderer[Any] {

    override protected[caliban] def unsafeRender(value: Any, indent: Option[Int], write: StringBuilder): Unit =
      if (indent.isDefined) write.append(' ')
  }

  /**
   * A Renderer which renders a newline character when in non-compact mode otherwise it renders a comma
   */
  lazy val newlineOrComma: Renderer[Any] = new Renderer[Any] {
    override protected[caliban] def unsafeRender(value: Any, indent: Option[Int], write: StringBuilder): Unit =
      if (indent.isDefined) write.append('\n') else write.append(',')
  }

  /**
   * A Renderer which renders a newline character when in non-compact mode otherwise it renders a space
   */
  lazy val newlineOrSpace: Renderer[Any] = new Renderer[Any] {
    override protected[caliban] def unsafeRender(value: Any, indent: Option[Int], write: StringBuilder): Unit =
      if (indent.isDefined) write.append('\n') else write.append(' ')
  }

  lazy val newlineOrEmpty: Renderer[Any] = new Renderer[Any] {
    override protected[caliban] def unsafeRender(value: Any, indent: Option[Int], write: StringBuilder): Unit =
      if (indent.isDefined) write.append('\n')
  }

  lazy val newline: Renderer[Any] = char('\n')

  def map[K, V](
    keyRender: Renderer[K],
    valueRender: Renderer[V],
    separator: Renderer[Any],
    delimiter: Renderer[Any]
  ): Renderer[Map[K, V]] = new Renderer[Map[K, V]] {
    override def unsafeRender(value: Map[K, V], indent: Option[Int], write: StringBuilder): Unit = {
      var first = true
      value.foreach { case (k, v) =>
        if (first) first = false
        else separator.unsafeRender((), indent, write)
        keyRender.unsafeRender(k, indent, write)
        delimiter.unsafeRender((), indent, write)
        valueRender.unsafeRender(v, indent, write)
      }
    }
  }

  private final case class Combined[-A](renderers: List[Renderer[A]]) extends Renderer[A] {
    override def unsafeRender(value: A, indent: Option[Int], write: StringBuilder): Unit =
      renderers.foreach(_.unsafeRender(value, indent, write))
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy