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

com.github.andyglow.json.Value.scala Maven / Gradle / Ivy

package com.github.andyglow.json

import com.github.andyglow.scalamigration._
import scala.collection._
import comparison._

sealed trait Value {

  def toString: String

  def tpe: String

  final def diff(y: Value): Result = internalDiff(y)

  protected def internalDiff(y: Value, path: Path = Path.Empty): Result
}

object Value {

  case object `null` extends Value {
    override def toString = "null"
    override def tpe      = "null"
    protected def internalDiff(y: Value, path: Path = Path.Empty): Result = y match {
      case `null` => Result.Equal
      case _      => Result.Different(Diff.ValueMismatch(path, `null`, y))
    }
  }

  sealed abstract class bool(val value: Boolean) extends Value with Product with Serializable {

    override def tpe = "boolean"

    def canEqual(that: Any): Boolean = that.isInstanceOf[bool]

    override def equals(that: Any): Boolean =
      canEqual(that) && (this.value == that.asInstanceOf[bool].value)

    override def hashCode: Int = value.hashCode

    protected def internalDiff(y: Value, path: Path = Path.Empty): Result = y match {
      case `true` if this == `true`   => Result.Equal
      case `false` if this == `false` => Result.Equal
      case bool(y)                    => Result.Different(Diff.ValueMismatch(path, value, y))
      case _                          => Result.Different(Diff.TypeMismatch(path, this.tpe, y.tpe))
    }
  }

  case object `true` extends bool(true) { override def toString = "true" }

  case object `false` extends bool(false) { override def toString = "false" }

  object bool {

    def apply(value: Boolean): bool = if (value) `true` else `false`

    def unapply(b: bool): Option[Boolean] = Some(b.value)
  }

  case class num(value: BigDecimal) extends Value {

    override def tpe = "number"

    override def toString = s"$value"

    protected def internalDiff(y: Value, path: Path = Path.Empty): Result = y match {
      case num(y) if y == value => Result.Equal
      case num(y)               => Result.Different(Diff.ValueMismatch(path, value, y))
      case _                    => Result.Different(Diff.TypeMismatch(path, this.tpe, y.tpe))
    }
  }

  object num {

    def apply(x: Byte): num = new num(x: Long)

    def apply(x: Int): num = new num(x)

    def apply(x: Long): num = new num(x)

    def apply(x: Short): num = new num(x.toInt)

    def apply(x: Float): num = new num(x.toDouble)

    def apply(x: Double): num = new num(x)

    def apply(x: BigInt): num = new num(BigDecimal(x))

    def apply(x: Number): num = new num(BigDecimal(x.doubleValue()))
  }

  case class str(value: String) extends Value {

    override def tpe = "string"

    override def toString = s""""$value""""

    protected def internalDiff(y: Value, path: Path = Path.Empty): Result = y match {
      case str(y) if y == value => Result.Equal
      case str(y)               => Result.Different(Diff.ValueMismatch(path, value, y))
      case _                    => Result.Different(Diff.TypeMismatch(path, this.tpe, y.tpe))
    }
  }

  case class arr(value: Seq[Value] = Seq.empty) extends Value {

    override def tpe = "array"

    def ++(other: arr): arr = arr(value ++ other.value)

    def :+(el: Value): arr = arr(value :+ el)

    def append(el: Value): arr = this.:+(el)

    def +:(el: Value): arr = arr(el +: value)

    def prepend(el: Value): arr = this.+:(el)

    override def toString: String = value map { _.toString } mkString ("[", ", ", "]")

    def contains(that: arr): Boolean = {
      that.value.length == this.value.length &&
      that.value.forall { thatV =>
        this.value.exists { thisV =>
          (thatV, thisV) match {
            case (l: obj, r: obj) => l contains r
            case (l: arr, r: arr) => l contains r
            case _                => thisV == thatV
          }
        }
      }
    }

    // TODO: implement bi-directional comparison
    protected def internalDiff(y: Value, path: Path = Path.Empty): Result = y match {
      case arr(y) =>
        if (y.length != this.value.length)
          Result.Different(Diff.ArrayLengthMismatch(path, this.value.length, y.length))
        else {
          var res: Result = Result.Equal
          // get through all right elements
          for { (y, idx) <- y.zipWithIndex } {
            // make sure they exists in left array
            val found = this.value.exists { x =>
              x.internalDiff(y, path) == Result.Equal
            }
            // update resulting diff with missing element if not found
            if (!found) res = res + Diff.MissingElement(path / idx, y)
          }

          res
        }
      case _ => Result.Different(Diff.TypeMismatch(path, this.tpe, y.tpe))
    }
  }

  object arr {

    def empty: arr = arr(Seq.empty)

    def apply(x: Value, xs: Value*): arr = arr(x +: xs.toSeq)
  }

  case class obj(private val underlying: Map[String, Value]) extends Value {

    override def tpe = "object"

    lazy val fields: Seq[(String, Value)] = underlying.toSeq

    lazy val value: Map[String, Value] = underlying match {
      case m: immutable.Map[String, value] => m
      case m                               => m.toMap
    }

    def fieldSet: Set[(String, Value)] = fields.toSet

    def keys: Set[String] = underlying.keySet

    def values: Iterable[Value] = underlying.values

    def ++(other: obj): obj = obj(underlying ++ other.underlying)

    def ++(other: Option[obj]): obj = other.fold(this)(x => obj(underlying ++ x.underlying))

    def -(otherField: String): obj = obj(underlying.toMap - otherField)

    def +(otherField: (String, Value)): obj = obj(underlying.toMap + otherField)

    def canEqual(other: Any): Boolean = other.isInstanceOf[obj]

    override def hashCode: Int = fieldSet.hashCode()

    def deepMerge(other: obj): obj = {

      def merge(existingObject: obj, otherObject: obj): obj = {
        val result = existingObject.underlying ++ otherObject.underlying.map { case (otherKey, otherValue) =>
          val maybeExistingValue = existingObject.underlying.get(otherKey)
          val newValue = (maybeExistingValue, otherValue) match {
            case (Some(e: obj), o: obj) => merge(e, o)
            case _                      => otherValue
          }

          (otherKey, newValue)
        }

        obj(result)
      }

      merge(this, other)
    }

    def contains(other: obj): Boolean = {
      other.underlying forall { case (k, thatV) =>
        underlying.get(k) match {
          case None =>
            false
          case Some(thisV) =>
            (thatV, thisV) match {
              case (thisV: obj, thatV: obj) => thisV contains thatV
              case (thisV: arr, thatV: arr) => thisV contains thatV
              case _                        => thatV == thisV
            }
        }
      }
    }

    protected def internalDiff(that: Value, path: Path = Path.Empty): Result = that match {
      case obj(thatFields) =>
        // TODO: should we store comparison direction as a field?
        // like: missed on left, missed on right
        val missed = ((thatFields.immutableKeys -- underlying.immutableKeys).map(k => (k, thatFields(k))) ++
          (underlying.immutableKeys -- thatFields.immutableKeys).map(k => (k, underlying(k)))) map { case (k, v) =>
          Diff.MissingProperty(path / k, v)
        }
        val init = if (missed.isEmpty) Result.Equal else Result.Different(missed.toList)

        (thatFields.immutableKeys intersect underlying.immutableKeys).foldLeft[Result](init) { case (acc, k) =>
          val thisV = underlying(k)
          val thatV = thatFields(k)
          acc ++ thisV.internalDiff(thatV, path / k)
        }

      case _ => Result.Different(Diff.TypeMismatch(path, this.tpe, that.tpe))
    }

    override def equals(other: Any): Boolean = other match {
      case that @ obj(_) => (that canEqual this) && fieldSet == that.fieldSet
      case _             => false
    }

    override def toString = value map { case (k, v) =>
      s""""$k": ${v.toString}"""
    } mkString ("{", ", ", "}")
  }

  object obj {

    sealed trait field

    object field {

      case object none extends field

      case class some(name: String, value: Value) extends field
    }

    def apply(fields: field*): obj = new obj(mutable.LinkedHashMap(fields collect { case field.some(k, v) =>
      (k, v)
    }: _*))

    val empty: obj = obj()
  }

  sealed trait FieldAdapter[T] {

    def adapt(x: (String, T)): obj.field
  }

  sealed trait ValueAdapter[T] {

    type J <: Value

    def adapt(x: T): J
  }

  trait LowPriorityValueAdapters {

    implicit object booleanAdapter extends ValueAdapter[Boolean] {
      type J = bool
      def adapt(x: Boolean): bool = bool(x)
    }

    implicit object stringAdapter extends ValueAdapter[String] {
      type J = str
      def adapt(x: String): str = str(x)
    }

    implicit def numberAdapter[T](implicit n: Numeric[T]): ValueAdapter[T] = new ValueAdapter[T] {
      type J = num
      def adapt(x: T): num = x match {
        case x: Int => num(x)
        case _      => num(n.toDouble(x))
      }
    }
  }

  trait LowPriorityFieldAdapters {

    implicit def tupleAdapter[T](implicit a: ValueAdapter[T]): FieldAdapter[T] =
      new FieldAdapter[T] {

        def adapt(x: (String, T)): obj.field = {
          val (k, v) = x

          obj.field.some(k, a adapt v)
        }
      }

    implicit def optTupleAdapter[T](implicit a: ValueAdapter[T]): FieldAdapter[Option[T]] =
      new FieldAdapter[Option[T]] {

        def adapt(x: (String, Option[T])): obj.field = x._2 match {
          case Some(v) => obj.field.some(x._1, a.adapt(v))
          case None    => obj.field.none
        }
      }
  }

  object ValueAdapter extends LowPriorityValueAdapters

  object FieldAdapter extends LowPriorityFieldAdapters

  import scala.language.implicitConversions

  implicit def convertValue[T](x: T)(implicit adapter: ValueAdapter[T]): adapter.J = adapter adapt x

  implicit def convertField[T](x: (String, T))(implicit adapter: FieldAdapter[T]): obj.field =
    adapter adapt x

  implicit def convertValueField[T](x: (String, Value)): obj.field = obj.field.some(x._1, x._2)

  implicit def convertOptValueField[T](x: (String, Option[Value])): obj.field = x match {
    case (f, Some(v)) => obj.field.some(f, v)
    case (_, None)    => obj.field.none
  }

  implicit class IterableOps[T](val x: Iterable[T]) {

    def toArr(implicit elementAdapter: ValueAdapter[T]): arr = arr(x.map {
      elementAdapter adapt _
    }.toSeq)
  }

  implicit val valueOrdering: Ordering[Value] = {
    Ordering fromLessThan {
      case (l: str, r: str)   => l.value < r.value
      case (l: num, r: num)   => l.value < r.value
      case (l: bool, r: bool) => l.value < r.value
      case (l: arr, r: arr) =>
        if (l.value.length == r.value.length)
          l.value.zip(r.value).forall { case (l, r) => valueOrdering.lt(l, r) }
        else
          l.value.length < r.value.length
      case (l: obj, r: obj) =>
        if (l.value.keySet == r.value.keySet) {
          val zipped = l.value.foldLeft[List[(Value, Value)]](Nil) { case (acc, (field, ll)) =>
            val rr = r.value(field)
            acc :+ ((ll, rr))
          }
          zipped.forall { case (l, r) => valueOrdering.lt(l, r) }
        } else
          l.value.keySet.toSeq.sorted.mkString < r.value.keySet.toSeq.sorted.mkString
      case (`null`, _) => true
      case (l, r) =>
        def weight(x: Value): Int = x match {
          case `null`  => 100
          case _: bool => 50
          case _: num  => 40
          case _: str  => 30
          case _: arr  => 20
          case _: obj  => 10
        }
        weight(l) < weight(r)
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy