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

izumi.fundamentals.platform.strings.TextTree.scala Maven / Gradle / Ivy

package izumi.fundamentals.platform.strings

import izumi.fundamentals.collections.nonempty.NEList
import izumi.fundamentals.platform.strings.IzString.*

import scala.language.implicitConversions

/** This is a convenience utility allowing to build trees of plain text and typed values.
  *
  * This utility is extremely useful for various template engines, query builders and transpilers
  */
sealed trait TextTree[+T]

object TextTree {
  def value[T](value: T): TextTree[T] = ValueNode(value)

  def text[T](value: String): TextTree[T] = StringNode(value)

  case class ValueNode[+T](value: T) extends TextTree[T]

  case class StringNode(value: String) extends TextTree[Nothing]

  case class Node[+T](chunks: NEList[TextTree[T]]) extends TextTree[T]

  case class Shift[+T](nested: TextTree[T], shift: Int) extends TextTree[T]

  case class Trim[+T](nested: TextTree[T]) extends TextTree[T]

  implicit class TextTreeSeqOps[T](target: Seq[TextTree[T]]) {
    def join(sep: String): TextTree[T] = {
      if (target.isEmpty) {
        StringNode("")
      } else {
        NEList.from(target.flatMap(t => Seq(t, StringNode(sep))).init) match {
          case Some(value) =>
            Node(value)
          case None =>
            StringNode("")
        }
      }
    }

    def join(begin: String, sep: String, end: String, shift: Option[Int] = Some(2)): TextTree[T] = {
      val joined = target.join(sep)
      val middle = shift match {
        case Some(value) => joined.shift(value)
        case None => joined
      }
      q"$begin$middle$end"
    }
  }

  implicit final class TextTreeGenericOps[T](private val target: TextTree[T]) {
    def as[W](implicit conv: T => W): TextTree[W] = {
      target.map(conv)
    }

    def dump: String = mapRender(_.toString)

    def mapRender(f: T => String): String = {
      target match {
        case v: ValueNode[T] => f(v.value)
        case s: StringNode => StringContext.processEscapes(s.value)
        case s: Shift[T] => s.nested.mapRender(f).shift(s.shift)
        case t: Trim[T] => t.nested.mapRender(f).trim
        case n: Node[T] => n.chunks.map(_.mapRender(f)).mkString
      }
    }

    def isEmpty: Boolean = {
      target match {
        case _: ValueNode[T] => false
        case s: StringNode => s.value.isEmpty
        case s: Shift[T] => s.nested.isEmpty
        case t: Trim[T] => t.nested.isEmpty
        case n: Node[T] =>
          n.chunks.forall(_.isEmpty)
      }
    }

    def nonEmpty: Boolean = !isEmpty

    def flatten: TextTree[T] = {
      target match {
        case v: ValueNode[T] => Node(NEList(v))
        case s: StringNode => Node(NEList(s))
        case s: Shift[T] => Shift(s.flatten, s.shift)
        case t: Trim[T] => Trim(t.flatten)
        case n: Node[T] =>
          Node(n.chunks.flatMap {
            _.flatten match {
              case n: Node[T] => n.chunks
              case o => NEList(o)
            }
          })
      }
    }

    def map[U](f: T => U): TextTree[U] = {
      target match {
        case v: ValueNode[T] => ValueNode(f(v.value))
        case s: StringNode => StringNode(s.value)
        case s: Shift[T] => Shift(s.nested.map(f), s.shift)
        case s: Trim[T] => Trim(s.nested.map(f))
        case n: Node[T] => Node(n.chunks.map(_.map(f)))
      }
    }

    def foreach(f: T => Unit): Unit = {
      target match {
        case v: ValueNode[T] => f(v.value)
        case _: StringNode => ()
        case s: Shift[T] => s.nested.foreach(f)
        case s: Trim[T] => s.nested.foreach(f)
        case n: Node[T] => n.chunks.foreach(_.foreach(f))
      }
    }

    def values: Seq[T] = {
      target match {
        case v: ValueNode[T] => Seq(v.value)
        case _: StringNode => Seq.empty
        case t: Trim[T] => t.nested.values
        case s: Shift[T] => s.nested.values
        case n: Node[T] => n.chunks.toSeq.flatMap(_.values)
      }
    }

    def stripMargin(marginChar: Char): TextTree[T] = {
      target match {
        case v: ValueNode[T] => v
        case s: StringNode => s
        case s: Shift[T] => s
        case t: Trim[T] => t
        case n: Node[T] =>
          Node(n.chunks.map {
            case v: ValueNode[T] => v
            case n: Node[T] => n
            case s: Shift[T] => s
            case t: Trim[T] => t
            case s: StringNode => StringNode(s.value.stripMargin(marginChar))
          })
      }
    }

    def stripMargin: TextTree[T] = stripMargin('|')

    def trim: TextTree[T] = {
      Trim(target)
    }

    def shift(pad: Int): TextTree[T] = {
      Shift(target, pad)
    }
  }

  implicit class Quote(val sc: StringContext) extends AnyVal {
    def q[T](args: InterpolationArg[T]*): TextTree[T] = {
      assert(sc.parts.length == args.length + 1)
      val seq = sc.parts
        .zip(args)
        .flatMap {
          case (t, v) =>
            List(StringNode(t), v.asNode)
        }
        .reverse

      Node(NEList(StringNode(sc.parts.last), seq).reverse)
    }
  }

  trait InterpolationArg[+T] {
    def asNode: TextTree[T]
  }

  object InterpolationArg extends LowPrioInterpolationArg_1 {}

  protected trait LowPrioInterpolationArg_1 extends LowPrioInterpolationArg_2 {
    implicit def arg_from_String[T](t: String): InterpolationArg[T] = new InterpolationArg[T] {
      override def asNode: TextTree[T] = StringNode(t)
    }

    implicit def arg_from_Nothing[T](
      node: TextTree[Nothing]
    ): InterpolationArg[T] = new InterpolationArg[T] {
      override def asNode: TextTree[T] = node.asInstanceOf[TextTree[T]]
    }
  }

  protected trait LowPrioInterpolationArg_2 {
    implicit def value[T](t: T): InterpolationArg[T] = new InterpolationArg[T] {
      override def asNode: TextTree[T] = ValueNode(t)
    }

    implicit def subtree[T](node: TextTree[T]): InterpolationArg[T] = new InterpolationArg[T] {
      override def asNode: TextTree[T] = node
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy