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

com.github.andyglow.jsonschema.SchemaEquality.scala Maven / Gradle / Ivy

package com.github.andyglow.jsonschema

import json.Schema
import Schema.`object`
import Schema.`def`
import Schema.`oneof`
import Schema.`allof`
import Schema.`not`
import`object`.Field

import com.github.andyglow.scalamigration._

sealed trait SchemaEquality

object SchemaEquality {
  private type F = Field[_]

  sealed trait Segment
  object Segment {
    case class Field(name: String) extends Segment
    case class Def(ref: String) extends Segment
    case class Schema(typeName: String) extends Segment
  }

  type Path = List[Segment]

  case object Equal extends SchemaEquality
  case class UnEqual(diff: Difference) extends SchemaEquality

  sealed trait Difference {
    def toDebugString: String
  }

  object Difference {

    case class NotMatchingFieldOrder(path: Path, left: List[String], right: List[String]) extends Difference {
      override def toDebugString: String = "field order mismatch at " + path.mkString(".") + ", Left (" + left.mkString(".") + "), Right(" + right.mkString(".") + ")"
    }

    case class MissingFields(path: Path, leftMisses: Set[F], rightMisses: Set[F]) extends Difference {
      override def toDebugString: String = {
        val sb = new StringBuilder("missed fields at ").append(path.mkString("."))
        if (leftMisses.nonEmpty) sb.append(". Left misses: (").append(leftMisses.map(_.name).mkString(".")).append(")")
        if (rightMisses.nonEmpty) sb.append(". Right misses: (").append(rightMisses.map(_.name).mkString(".")).append(")")
        sb.toString()
      }
    }

    case class MissingVariants(path: Path, leftMisses: Set[Schema[_]], rightMisses: Set[Schema[_]]) extends Difference {
      override def toDebugString: String = {
        val sb = new StringBuilder("missed variants at ").append(path.mkString("."))
        if (leftMisses.nonEmpty) sb.append(". Left misses: (").append(leftMisses.map(_.toString).mkString(".")).append(")")
        if (rightMisses.nonEmpty) sb.append(". Right misses: (").append(rightMisses.map(_.toString).mkString(".")).append(")")
        sb.toString()
      }
    }

    case class DefinitionSignatureMismatch(path: Path, left: String, right: String) extends Difference {
      override def toDebugString: String = "def signature mismatch at " + path.mkString(".")
    }

    case class FieldRequirednessMismatch(path: Path, left: Boolean, right: Boolean) extends Difference {
      override def toDebugString: String = "field requiredness mismatch at " + path.mkString(".")
    }

    case class ClashingSchemas(path: Path, left: Schema[_], right: Schema[_]) extends Difference {
      override def toDebugString: String = "schemas mismatch at " + path.mkString(".")
    }
  }

  import Difference._

  case class Context(checkOrder: Boolean) {
    def compute(l: Schema[_], r: Schema[_]): SchemaEquality = internalCompute(Nil, l, r, ctx = this)
  }

  def apply(checkOrder: Boolean = true): Context = Context(checkOrder)
  def unordered: Context = Context(checkOrder = false)
  def ordered: Context = Context(checkOrder = true)

  def compute(l: Schema[_], r: Schema[_]): SchemaEquality = ordered.compute(l, r)

  private def internalCompute(path: Path, l: Schema[_], r: Schema[_], ctx: Context): SchemaEquality = (l, r) match {

    case (l: `object`[_], r: `object`[_]) =>
      val res: Either[Difference, SchemaEquality] = for {
        _ <- checkMissingFields(path, l, r)
        _ <- if (ctx.checkOrder) checkMatchingFieldOrder(path, l, r) else Right(())
        _ <- checkFieldAttributes(path, l, r, ctx)
      } yield { Equal }

      res.left.map[SchemaEquality](UnEqual).merge

    case (l: `def`[_], r: `def`[_]) =>
      if (l.sig == r.sig) {
        internalCompute(path :+ Segment.Def(l.sig), l.tpe, r.tpe, ctx)
      } else
        UnEqual(DefinitionSignatureMismatch(path, l.sig, r.sig))

    case (l: `not`[_], r: `not`[_]) =>
      internalCompute(path :+ Segment.Schema("not"), l.tpe, r.tpe, ctx)

    case (l: `oneof`[_], r: `oneof`[_]) =>
      val leftMisses  = r.subTypes -- l.subTypes
      val rightMisses = l.subTypes -- r.subTypes

      if (leftMisses.nonEmpty || rightMisses.nonEmpty) UnEqual(MissingVariants(path :+ Segment.Schema("oneof"), leftMisses, rightMisses)) else Equal

    case (l: `allof`[_], r: `allof`[_]) =>
      val leftMisses  = r.subTypes -- l.subTypes
      val rightMisses = l.subTypes -- r.subTypes

      if (leftMisses.nonEmpty || rightMisses.nonEmpty) UnEqual(MissingVariants(path :+ Segment.Schema("allof"), leftMisses, rightMisses)) else Equal

    case (l, r) =>
      if (l == r) Equal else UnEqual(ClashingSchemas(path, l, r))
  }


  private def checkMissingFields(path: Path, l: `object`[_], r: `object`[_]): Either[MissingFields, Unit] = {
    val lf = l.fields.map(f => f.name -> f).toMap[String, F]
    val rf = r.fields.map(f => f.name -> f).toMap[String, F]

    val leftMisses  = rf.keySet -- lf.keySet
    val rightMisses = lf.keySet -- rf.keySet

    if (leftMisses.nonEmpty || rightMisses.nonEmpty) Left(MissingFields(path, leftMisses.map(rf), rightMisses.map(lf))) else Right(())
  }

  // check attributes
  // - required
  // - tpe (schema)
  private def checkFieldAttributes(path: Path, l: `object`[_], r: `object`[_], ctx: Context): Either[Difference, Unit] = {
    val lf = l.fields.map(f => f.name -> f).toMap[String, F]
    val rf = r.fields.map(f => f.name -> f).toMap[String, F]

    val fields  = rf.keySet intersect lf.keySet

    fields.foldLeft[Either[Difference, Unit]](Right(())) {
      case (Left(diff), _) => Left(diff)
      case (Right(_), fn) =>
        val ll = lf(fn)
        val rr = rf(fn)

        if (ll.required != rr.required) Left(FieldRequirednessMismatch(path :+ Segment.Field(fn), ll.required, rr.required)) else {
          internalCompute(path :+ Segment.Field(fn), ll.tpe, rr.tpe, ctx) match {
            case Equal => Right(())
            case UnEqual(diff) => Left(diff)
          }
        }
    }
  }

  private def checkMatchingFieldOrder(path: Path, l: `object`[_], r: `object`[_]): Either[NotMatchingFieldOrder, Unit] = {
    val lf = l.fields.map(_.name)
    val rf = r.fields.map(_.name)

    if (lf == rf) Right(()) else Left(NotMatchingFieldOrder(path, lf, rf))
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy