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

caliban.validation.FragmentValidator.scala Maven / Gradle / Ivy

The newest version!
package caliban.validation

import caliban.CalibanError.ValidationError
import caliban.introspection.adt._
import caliban.parsing.adt.Selection
import caliban.validation.Utils._
import zio.Chunk

import scala.collection.mutable
import scala.util.hashing.MurmurHash3

object FragmentValidator {
  def findConflictsWithinSelectionSet(
    context: Context,
    parentType: __Type,
    selectionSet: List[Selection]
  ): Either[ValidationError, Unit] = {

    // NOTE: We use the `hashCode()` as the key since it's much more performant
    val shapeCache   = mutable.Map.empty[Int, Chunk[String]]
    val parentsCache = mutable.Map.empty[Int, Chunk[String]]
    val groupsCache  = mutable.Map.empty[Int, Chunk[Set[SelectedField]]]

    def sameResponseShapeByName(set: Iterable[Selection]): Chunk[String] = {
      val keyHash = MurmurHash3.unorderedHash(set)
      shapeCache.get(keyHash) match {
        case Some(value) => value
        case None        =>
          val fields = FieldMap(context, parentType, set)
          val res    = Chunk.fromIterable(fields.flatMap { case (name, values) =>
            cross(values, includeIdentity = true).flatMap { case (f1, f2) =>
              if (doTypesConflict(f1.fieldDef._type, f2.fieldDef._type)) {
                Chunk(
                  s"$name has conflicting types: ${f1.parentType.name.getOrElse("")}.${f1.fieldDef.name} and ${f2.parentType.name
                      .getOrElse("")}.${f2.fieldDef.name}. Try using an alias."
                )
              } else
                sameResponseShapeByName(f1.selection.selectionSet ::: f2.selection.selectionSet)
            }
          })
          shapeCache.update(keyHash, res)
          res
      }
    }

    def sameForCommonParentsByName(set: Iterable[Selection]): Chunk[String] = {
      val keyHash = MurmurHash3.unorderedHash(set)
      parentsCache.get(keyHash) match {
        case Some(value) => value
        case None        =>
          val fields = FieldMap(context, parentType, set)
          val res    = Chunk.fromIterable(fields.flatMap { case (_, fields) =>
            groupByCommonParents(fields).flatMap { group =>
              val merged = group.flatMap(_.selection.selectionSet)
              requireSameNameAndArguments(group) ++ sameForCommonParentsByName(merged)
            }
          })
          parentsCache.update(keyHash, res)
          res
      }
    }

    def doTypesConflict(t1: __Type, t2: __Type): Boolean =
      if (isNonNull(t1))
        if (isNonNull(t2)) t1.ofType.flatMap(p1 => t2.ofType.map(p2 => doTypesConflict(p1, p2))).getOrElse(true)
        else true
      else if (isNonNull(t2))
        true
      else if (isListType(t1))
        if (isListType(t2)) t1.ofType.flatMap(p1 => t2.ofType.map(p2 => doTypesConflict(p1, p2))).getOrElse(true)
        else true
      else if (isListType(t2))
        true
      else if (isLeafType(t1) && isLeafType(t2)) {
        t1.name != t2.name
      } else if (!isComposite(t1) || !isComposite(t2))
        true
      else
        false

    def requireSameNameAndArguments(fields: Set[SelectedField]) =
      cross(fields, includeIdentity = false).flatMap { case (f1, f2) =>
        if (f1.fieldDef.name != f2.fieldDef.name) {
          Some(
            s"${f1.parentType.name.getOrElse("")}.${f1.fieldDef.name} and ${f2.parentType.name.getOrElse("")}.${f2.fieldDef.name} are different fields."
          )
        } else if (f1.selection.arguments != f2.selection.arguments)
          Some(s"${f1.fieldDef.name} and ${f2.fieldDef.name} have different arguments")
        else None
      }

    def groupByCommonParents(fields: Set[SelectedField]): Chunk[Set[SelectedField]] = {
      val keyHash = fields.hashCode()
      groupsCache.get(keyHash) match {
        case Some(value) => value
        case None        =>
          val abstractGroup = fields.collect {
            case field if !isConcrete(field.parentType) => field
          }

          val concreteGroups =
            mutable.Map.empty[String, mutable.Builder[SelectedField, Set[SelectedField]]]

          fields.foreach {
            case field @ SelectedField(
                  __Type(_, Some(name), _, _, _, _, _, _, _, _, _, _, _),
                  _,
                  _
                ) if isConcrete(field.parentType) =>
              concreteGroups.getOrElseUpdate(name, Set.newBuilder ++= abstractGroup) += field
            case _ => ()
          }

          val res =
            if (concreteGroups.isEmpty) Chunk(fields)
            else Chunk.fromIterable(concreteGroups.values.map(_.result()))

          groupsCache.update(keyHash, res)
          res
      }
    }

    val conflicts = sameResponseShapeByName(selectionSet) ++ sameForCommonParentsByName(selectionSet)
    if (conflicts.nonEmpty) {
      Left(ValidationError(conflicts.head, ""))
    } else {
      ValidationOps.unit
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy