diff --git a/build.sbt b/build.sbt index 1dd51a5da7..59f03bdd0d 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,4 @@ -import com.typesafe.tools.mima.core.{ IncompatibleMethTypeProblem, IncompatibleResultTypeProblem, ProblemFilters } +import com.typesafe.tools.mima.core.* import org.scalajs.linker.interface.ModuleSplitStyle import sbtcrossproject.CrossPlugin.autoImport.{ crossProject, CrossType } @@ -676,7 +676,10 @@ lazy val enableMimaSettingsJVM = mimaPreviousArtifacts := previousStableVersion.value.map(organization.value %% moduleName.value % _).toSet, mimaBinaryIssueFilters ++= Seq( ProblemFilters.exclude[IncompatibleMethTypeProblem]("caliban.schema.Step#ObjectStep*"), - ProblemFilters.exclude[IncompatibleResultTypeProblem]("caliban.schema.Step#ObjectStep*") + ProblemFilters.exclude[IncompatibleResultTypeProblem]("caliban.schema.Step#ObjectStep*"), + ProblemFilters.exclude[DirectMissingMethodProblem]("caliban.schema.Annotations*"), + ProblemFilters.exclude[MissingTypesProblem]("caliban.schema.Annotations*"), + ProblemFilters.exclude[IncompatibleResultTypeProblem]("caliban.schema.Annotations*") ) ) diff --git a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala index 1c8aa42f9f..aebe904985 100644 --- a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala @@ -125,8 +125,8 @@ trait CommonSchemaDerivation[R] { case _ => false } val isInterface = ctx.annotations.exists { - case GQLInterface() => true - case _ => false + case _: GQLInterface => true + case _ => false } val isUnion = ctx.annotations.exists { case GQLUnion() => true @@ -158,18 +158,19 @@ trait CommonSchemaDerivation[R] { Some(getDirectives(ctx.annotations)) ) else { + val excl = ctx.annotations.collectFirst { case i: GQLInterface => i.excludedFields.toSet }.getOrElse(Set.empty) val impl = subtypes.map(_._1.copy(interfaces = () => Some(List(toType(isInput, isSubscription))))) val commonFields = () => impl .flatMap(_.allFields) .groupBy(_.name) - .filter { case (_, list) => list.lengthCompare(impl.size) == 0 } - .collect { case (_, list) => - Types - .unify(list) - .flatMap(t => - list.headOption.map(_.copy(description = Types.extractCommonDescription(list), `type` = () => t)) - ) + .collect { + case (name, list) if list.lengthCompare(impl.size) == 0 && !excl.contains(name) => + Types + .unify(list) + .flatMap(t => + list.headOption.map(_.copy(description = Types.extractCommonDescription(list), `type` = () => t)) + ) } .flatten .toList diff --git a/core/src/main/scala-3/caliban/schema/DerivationUtils.scala b/core/src/main/scala-3/caliban/schema/DerivationUtils.scala index 03742b9225..4e180615c8 100644 --- a/core/src/main/scala-3/caliban/schema/DerivationUtils.scala +++ b/core/src/main/scala-3/caliban/schema/DerivationUtils.scala @@ -88,12 +88,13 @@ private object DerivationUtils { info: TypeInfo, impl: List[__Type] ): __Type = { + val excl = annotations.collectFirst { case i: GQLInterface => i.excludedFields.toSet }.getOrElse(Set.empty) val commonFields = () => impl .flatMap(_.allFields) .groupBy(_.name) .collect { - case (_, list) if list.lengthCompare(impl.size) == 0 => + case (name, list) if list.lengthCompare(impl.size) == 0 && !excl.contains(name) => Types .unify(list) .flatMap(t => diff --git a/core/src/main/scala-3/caliban/schema/SumSchema.scala b/core/src/main/scala-3/caliban/schema/SumSchema.scala index c4528a2894..3a45f6ada7 100644 --- a/core/src/main/scala-3/caliban/schema/SumSchema.scala +++ b/core/src/main/scala-3/caliban/schema/SumSchema.scala @@ -19,7 +19,7 @@ final private class SumSchema[R, A]( } private lazy val isEnum = subTypes.forall((_, t, _) => t.allFields.isEmpty && t.allInputFields.isEmpty) - private val isInterface = annotations.contains(GQLInterface()) + private val isInterface = annotations.exists(_.isInstanceOf[GQLInterface]) private val isUnion = annotations.contains(GQLUnion()) def toType(isInput: Boolean, isSubscription: Boolean): __Type = diff --git a/core/src/main/scala/caliban/schema/Annotations.scala b/core/src/main/scala/caliban/schema/Annotations.scala index 9689a206ac..16f861e9ad 100644 --- a/core/src/main/scala/caliban/schema/Annotations.scala +++ b/core/src/main/scala/caliban/schema/Annotations.scala @@ -44,8 +44,10 @@ object Annotations { /** * Annotation to make a sealed trait an interface instead of a union type or an enum + * + * @param excludeFields Optionally provide a list of field names that should be excluded from the interface */ - case class GQLInterface() extends StaticAnnotation + case class GQLInterface(excludedFields: String*) extends StaticAnnotation /** * Annotation to make a sealed trait a union instead of an enum diff --git a/core/src/test/scala/caliban/schema/SchemaSpec.scala b/core/src/test/scala/caliban/schema/SchemaSpec.scala index 86e5b82119..a90e2f48fb 100644 --- a/core/src/test/scala/caliban/schema/SchemaSpec.scala +++ b/core/src/test/scala/caliban/schema/SchemaSpec.scala @@ -89,6 +89,11 @@ object SchemaSpec extends ZIOSpecDefault { test("interface only take fields that return the same type") { assertTrue(introspect[MyInterface].fields(__DeprecatedArgs()).toList.flatten.map(_.name) == List("c1", "c2")) }, + test("excluding fields from interfaces") { + assertTrue( + introspect[InterfaceWithExclusions].fields(__DeprecatedArgs()).toList.flatten.map(_.name) == List("c1") + ) + }, test("enum-like sealed traits annotated with GQLUnion") { assert(introspect[EnumLikeUnion])( hasField[__Type, __TypeKind]("kind", _.kind, equalTo(__TypeKind.UNION)) @@ -381,14 +386,21 @@ object SchemaSpec extends ZIOSpecDefault { @GQLInterface sealed trait MyInterface - object MyInterface { + object MyInterface { case class A(c1: Int, c2: Int => Int, d1: Int, d2: Int => Int, d3: Int => Int) extends MyInterface case class B(c1: Int, c2: Int => Int, d1: Boolean, d2: Int, d3: Option[Int] => Int) extends MyInterface } + @GQLInterface(excludedFields = "c2", "c3") + sealed trait InterfaceWithExclusions + object InterfaceWithExclusions { + case class A(c1: Int, c2: String, c3: Int, c5: Int) extends InterfaceWithExclusions + case class B(c1: Int, c2: String, c3: Int, c4: Int) extends InterfaceWithExclusions + } + @GQLUnion sealed trait EnumLikeUnion - object EnumLikeUnion { + object EnumLikeUnion { case object A extends EnumLikeUnion case object B extends EnumLikeUnion }