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

caliban.Value.scala Maven / Gradle / Ivy

The newest version!
package caliban

import caliban.Value.StringValue
import caliban.interop.tapir.IsTapirSchema
import caliban.rendering.ValueRenderer
import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec
import zio.stream.Stream

import scala.util.control.NonFatal
import scala.util.hashing.MurmurHash3

sealed trait InputValue extends Serializable { self =>
  def toInputString: String = ValueRenderer.inputValueRenderer.renderCompact(self)
}
object InputValue {
  case class ListValue(values: List[InputValue])          extends InputValue {
    override def toString: String      = values.mkString("[", ",", "]")
    override def toInputString: String = ValueRenderer.inputListValueRenderer.render(this)
  }
  case class ObjectValue(fields: Map[String, InputValue]) extends InputValue {
    override def toString: String =
      fields.map { case (name, value) => s""""$name":${value.toString}""" }.mkString("{", ",", "}")

    override def toInputString: String = ValueRenderer.inputObjectValueRenderer.render(this)
  }
  case class VariableValue(name: String)                  extends InputValue {
    override def toString: String = s"$$$name"
  }

  private[caliban] implicit def jsoniterCodec: JsonValueCodec[InputValue] =
    caliban.interop.jsoniter.ValueJsoniter.inputValueCodec
}

sealed trait ResponseValue extends Serializable { self =>

  /**
   * Performs a deep merge of two response values. This currently means that the list values will be concatenated, and
   * object values will be merged, incompatible types will assume the right-hand side of the merge.
   */
  def deepMerge(other: ResponseValue): ResponseValue = (this, other) match {
    case (ResponseValue.ObjectValue(o1), ResponseValue.ObjectValue(o2)) =>
      val otherMap = o2.toMap
      ResponseValue.ObjectValue(o1.map { case (k, v) =>
        otherMap.get(k) match {
          case Some(otherValue) => (k, v.deepMerge(otherValue))
          case None             => (k, v)
        }
      })
    case (ResponseValue.ListValue(l1), ResponseValue.ListValue(l2))     =>
      ResponseValue.ListValue(l1 ++ l2)
    case _                                                              => other
  }
}
object ResponseValue {

  def at(path: List[PathValue])(value: ResponseValue): ResponseValue = {
    def loop(path: List[PathValue], value: ResponseValue): ResponseValue = path match {
      case Nil                            => value
      case PathValue.Key(key) :: tail     =>
        value match {
          case ObjectValue(fields) =>
            fields.find(_._1 == key) match {
              case Some((_, v)) => loop(tail, v)
              case None         => Value.NullValue
            }
          case ListValue(values)   =>
            ListValue(values.map(loop(path, _)))
          case _                   => Value.NullValue
        }
      case PathValue.Index(index) :: tail =>
        value match {
          case ListValue(values) =>
            val idx = index
            if (idx < values.size) {
              loop(tail, values(idx))
            } else {
              Value.NullValue
            }
          case _                 => Value.NullValue
        }
      case _                              => Value.NullValue
    }

    loop(path, value)
  }

  case class ListValue(values: List[ResponseValue])                extends ResponseValue {
    override def toString: String = ValueRenderer.responseListValueRenderer.renderCompact(this)
  }
  case class ObjectValue(fields: List[(String, ResponseValue)])    extends ResponseValue {
    override def toString: String =
      ValueRenderer.responseObjectValueRenderer.renderCompact(this)

    @transient override lazy val hashCode: Int = MurmurHash3.unorderedHash(fields)
    override def equals(other: Any): Boolean   =
      other match {
        case o: ObjectValue => o.hashCode == hashCode
        case _              => false
      }
  }
  case class StreamValue(stream: Stream[Throwable, ResponseValue]) extends ResponseValue {
    override def toString: String = ""
  }

  implicit def tapirSchema[F[_]: IsTapirSchema]: F[ResponseValue] =
    caliban.interop.tapir.schema.responseValueSchema.asInstanceOf[F[ResponseValue]]

  private[caliban] implicit def jsoniterCodec: JsonValueCodec[ResponseValue] =
    caliban.interop.jsoniter.ValueJsoniter.responseValueCodec
}

sealed trait Value extends InputValue with ResponseValue
object Value {
  case object NullValue                   extends Value                {
    override def toString: String = "null"
  }
  sealed trait IntValue                   extends Value                {
    def toInt: Int
    def toLong: Long
    def toBigInt: BigInt
  }
  sealed trait FloatValue                 extends Value                {
    def toFloat: Float
    def toDouble: Double
    def toBigDecimal: BigDecimal
  }
  case class StringValue(value: String)   extends Value with PathValue {
    override def toString: String = s""""${value.replace("\"", "\\\"").replace("\n", "\\n")}""""
  }
  case class BooleanValue(value: Boolean) extends Value                {
    override def toString: String = if (value) "true" else "false"
  }
  case class EnumValue(value: String)     extends Value                {
    override def toString: String      = s""""${value.replace("\"", "\\\"")}""""
    override def toInputString: String = ValueRenderer.enumInputValueRenderer.render(this)
  }

  object IntValue {
    def apply(v: Int): IntValue    = IntNumber(v)
    def apply(v: Long): IntValue   = LongNumber(v)
    def apply(v: BigInt): IntValue = BigIntNumber(v)

    @deprecated("Use `fromStringUnsafe` instead", "2.5.0")
    def apply(s: String): IntValue = fromStringUnsafe(s)

    @throws[NumberFormatException]("if the string is not a valid representation of an integer")
    def fromStringUnsafe(s: String): IntValue =
      try {
        val mod  = if (s.charAt(0) == '-') 1 else 0
        val size = s.length - mod
        if (size < 10) IntNumber(s.toInt)
        else if (size < 19) LongNumber(s.toLong)
        else BigIntNumber(BigInt(s))
      } catch {
        case NonFatal(_) => BigIntNumber(BigInt(s)) // Should never happen, but we leave it as a fallback
      }

    final case class IntNumber(value: Int)       extends IntValue with PathValue {
      override def toInt: Int       = value
      override def toLong: Long     = value.toLong
      override def toBigInt: BigInt = BigInt(value)
      override def toString: String = value.toString
    }
    final case class LongNumber(value: Long)     extends IntValue                {
      override def toInt: Int       = value.toInt
      override def toLong: Long     = value
      override def toBigInt: BigInt = BigInt(value)
      override def toString: String = value.toString
    }
    final case class BigIntNumber(value: BigInt) extends IntValue                {
      override def toInt: Int       = value.toInt
      override def toLong: Long     = value.toLong
      override def toBigInt: BigInt = value
      override def toString: String = value.toString
    }
  }

  object FloatValue {
    def apply(v: Float): FloatValue      = FloatNumber(v)
    def apply(v: Double): FloatValue     = DoubleNumber(v)
    def apply(v: BigDecimal): FloatValue = BigDecimalNumber(v)

    @deprecated("Use `fromStringUnsafe` instead", "2.6.0")
    def apply(s: String): FloatValue = fromStringUnsafe(s)

    @throws[NumberFormatException]("if the string is not a valid representation of a float")
    def fromStringUnsafe(s: String): FloatValue =
      try DoubleNumber(s.toDouble)
      catch { case NonFatal(_) => BigDecimalNumber(BigDecimal(s)) }

    final case class FloatNumber(value: Float)           extends FloatValue {
      override def toFloat: Float           = value
      override def toDouble: Double         = value.toDouble
      override def toBigDecimal: BigDecimal = BigDecimal.decimal(value)
      override def toString: String         = value.toString
    }
    final case class DoubleNumber(value: Double)         extends FloatValue {
      override def toFloat: Float           = value.toFloat
      override def toDouble: Double         = value
      override def toBigDecimal: BigDecimal = BigDecimal(value)
      override def toString: String         = value.toString
    }
    final case class BigDecimalNumber(value: BigDecimal) extends FloatValue {
      override def toFloat: Float           = value.toFloat
      override def toDouble: Double         = value.toDouble
      override def toBigDecimal: BigDecimal = value
      override def toString: String         = value.toString
    }
  }
}

sealed trait PathValue extends Value {
  def isKey: Boolean = this match {
    case StringValue(_) => true
    case _              => false
  }
}

object PathValue {
  def fromEither(either: Either[String, Int]): PathValue = either.fold(Key.apply, Index.apply)

  object Key   {
    def apply(value: String): PathValue           = Value.StringValue(value)
    def unapply(value: PathValue): Option[String] = value match {
      case Value.StringValue(s) => Some(s)
      case _                    => None
    }
  }
  object Index {
    def apply(value: Int): PathValue           = Value.IntValue.IntNumber(value)
    def unapply(value: PathValue): Option[Int] = value match {
      case Value.IntValue.IntNumber(i) => Some(i)
      case _                           => None
    }
  }

  /**
   * This function parses a string and returns a PathValue.
   * If the string contains only digits, it returns a `PathValue.Index`.
   * Otherwise, it returns a `PathValue.Key`.
   *
   * @param value the string to parse
   * @return a PathValue which is either an `Index` if the string is numeric, or a `Key` otherwise
   */
  def parse(value: String): PathValue = {
    var i    = 0
    val size = value.length
    while (i < size) {
      val c = value.charAt(i)
      if (c >= '0' && c <= '9') i += 1
      else return PathValue.Key(value)
    }
    try PathValue.Index(value.toInt)
    catch { case NonFatal(_) => PathValue.Key(value) } // Should never happen, just a fallback
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy