diff --git a/core/src/main/scala/caliban/parsing/adt/Definition.scala b/core/src/main/scala/caliban/parsing/adt/Definition.scala index e9526aeac6..8b073eca1d 100644 --- a/core/src/main/scala/caliban/parsing/adt/Definition.scala +++ b/core/src/main/scala/caliban/parsing/adt/Definition.scala @@ -77,7 +77,7 @@ object Definition { } } - sealed trait TypeDefinition extends TypeSystemDefinition { + sealed trait TypeDefinition extends TypeSystemDefinition { def name: String def description: Option[String] def directives: List[Directive] @@ -85,6 +85,10 @@ object Definition { final def isDeprecated: Boolean = Directives.isDeprecated(directives) final def deprecationReason: Option[String] = Directives.deprecationReason(directives) } + private[caliban] sealed trait AggregationTypeDefinition extends TypeDefinition { + def fields: List[FieldDefinition] + def implements: List[NamedType] + } object TypeDefinition { @@ -94,7 +98,7 @@ object Definition { implements: List[NamedType], directives: List[Directive], fields: List[FieldDefinition] - ) extends TypeDefinition { + ) extends AggregationTypeDefinition { override def toString: String = "Object" } @@ -104,7 +108,7 @@ object Definition { implements: List[NamedType], directives: List[Directive], fields: List[FieldDefinition] - ) extends TypeDefinition { + ) extends AggregationTypeDefinition { override def toString: String = "Interface" } diff --git a/core/src/main/scala/caliban/parsing/adt/Document.scala b/core/src/main/scala/caliban/parsing/adt/Document.scala index 4977e4d807..60cd456e45 100644 --- a/core/src/main/scala/caliban/parsing/adt/Document.scala +++ b/core/src/main/scala/caliban/parsing/adt/Document.scala @@ -40,6 +40,8 @@ case class Document(definitions: List[Definition], sourceMapper: SourceMapper) { @transient lazy val subscriptionDefinitions: List[OperationDefinition] = definitions.collect { case od: OperationDefinition if od.operationType == Subscription => od } - def objectTypeDefinition(name: String): Option[ObjectTypeDefinition] = + def objectTypeDefinition(name: String): Option[ObjectTypeDefinition] = objectTypeDefinitions.find(t => t.name == name) + def interfaceTypeDefinition(name: String): Option[InterfaceTypeDefinition] = + interfaceTypeDefinitions.find(t => t.name == name) } diff --git a/tools/src/main/scala/caliban/tools/SchemaWriter.scala b/tools/src/main/scala/caliban/tools/SchemaWriter.scala index d6804fa678..358092f842 100644 --- a/tools/src/main/scala/caliban/tools/SchemaWriter.scala +++ b/tools/src/main/scala/caliban/tools/SchemaWriter.scala @@ -1,6 +1,7 @@ package caliban.tools import caliban.parsing.adt.Definition.TypeSystemDefinition.TypeDefinition +import caliban.parsing.adt.Definition.TypeSystemDefinition.AggregationTypeDefinition import caliban.parsing.adt.Definition.TypeSystemDefinition.TypeDefinition._ import caliban.parsing.adt.Directives.{ LazyDirective, NewtypeDirective } import caliban.parsing.adt.Type.{ ListType, NamedType } @@ -89,10 +90,10 @@ object SchemaWriter { s"${safeName(field.name)} :$argsTypeName $effect[$fieldType]" } - def isAbstractEffectful(typedef: ObjectTypeDefinition): Boolean = + def isAbstractEffectful(typedef: AggregationTypeDefinition): Boolean = isEffectTypeAbstract && isEffectful(typedef) - def isEffectful(typedef: ObjectTypeDefinition): Boolean = + def isEffectful(typedef: AggregationTypeDefinition): Boolean = isLocalEffectful(typedef) || isNestedEffectful(typedef) def isUnionSiblingAbstractEffectful(typedef: ObjectTypeDefinition): Boolean = @@ -102,19 +103,27 @@ object SchemaWriter { union.memberTypes.flatMap(schema.objectTypeDefinition).exists(isAbstractEffectful) ) - def isLocalEffectful(typedef: ObjectTypeDefinition): Boolean = + def isLocalEffectful(typedef: AggregationTypeDefinition): Boolean = hasFieldWithDirective(typedef, LazyDirective) - def isNestedEffectful(typedef: ObjectTypeDefinition): Boolean = + def isNestedEffectful(typedef: AggregationTypeDefinition): Boolean = typeNameToNestedFields .getOrElse(typedef.name, List.empty) .exists(t => hasFieldWithDirective(t, LazyDirective)) - def generic(op: ObjectTypeDefinition, isRootDefinition: Boolean = false): String = - if ((isRootDefinition && isEffectTypeAbstract) || isAbstractEffectful(op) || isUnionSiblingAbstractEffectful(op)) - s"[${effect}[_]]" - else - s"" + def generic(tpeDef: AggregationTypeDefinition, isRootDefinition: Boolean = false): String = + tpeDef match { + case op: ObjectTypeDefinition => + if ( + (isRootDefinition && isEffectTypeAbstract) || isAbstractEffectful(op) || isUnionSiblingAbstractEffectful(op) + ) + s"[${effect}[_]]" + else + "" + case interface: InterfaceTypeDefinition => + if (isAbstractEffectful(interface)) s"[${effect}[_]]" + else "" + } def writeRootQueryOrMutationDef(op: ObjectTypeDefinition): String = s""" @@ -209,7 +218,9 @@ object SchemaWriter { s"""@GQLInterface ${writeTypeAnnotations( interface - )}sealed trait ${interface.name} extends scala.Product with scala.Serializable $derivesEnvSchema { + )}sealed trait ${interface.name}${generic( + interface + )} extends scala.Product with scala.Serializable $derivesEnvSchema { ${interface.fields.map(field => writeField(field, interface, isMethod = true)).mkString("\n")} } """ @@ -449,8 +460,16 @@ object SchemaWriter { schemaDef.exists(_.subscription.getOrElse("Subscription") == obj.name) ) .map { obj => - val extendsInterfaces = obj.implements.map(name => name.name) - val partOfUnionTypes = unionTypes.collect { + val extendsInterfaces = obj.implements.flatMap { case NamedType(name, _) => + schema + .interfaceTypeDefinition(name) + .map(interface => + if (isAbstractEffectful(interface)) s"$name[F]" + else name + ) + } + + val partOfUnionTypes = unionTypes.collect { case (u, members) if members.exists(_.name == obj.name) => if (members.exists(isAbstractEffectful)) s"${u.name}[F]" else u.name } diff --git a/tools/src/test/resources/snapshots/SchemaWriterSpec/interface, abstracted effect and lazy combination.scala b/tools/src/test/resources/snapshots/SchemaWriterSpec/interface, abstracted effect and lazy combination.scala new file mode 100644 index 0000000000..374d9d11e3 --- /dev/null +++ b/tools/src/test/resources/snapshots/SchemaWriterSpec/interface, abstracted effect and lazy combination.scala @@ -0,0 +1,30 @@ +import Types._ + +import caliban.schema.Annotations._ + +object Types { + + final case class T0[F[_]](i: F[Int]) extends Direct[F] + final case class T1[F[_]](i: scala.Option[T2[F]]) extends Transitive[F] + final case class T2[F[_]](i: F[scala.Option[Int]]) + + @GQLInterface + sealed trait Direct[F[_]] extends scala.Product with scala.Serializable { + def i: F[Int] + } + + @GQLInterface + sealed trait Transitive[F[_]] extends scala.Product with scala.Serializable { + def i: scala.Option[T2[F]] + } + +} + +object Operations { + + final case class Query[F[_]]( + t0: F[T0[F]], + t1: F[T1[F]] + ) + +} diff --git a/tools/src/test/scala/caliban/tools/SchemaWriterSpec.scala b/tools/src/test/scala/caliban/tools/SchemaWriterSpec.scala index 71735c700c..5147a4bfd1 100644 --- a/tools/src/test/scala/caliban/tools/SchemaWriterSpec.scala +++ b/tools/src/test/scala/caliban/tools/SchemaWriterSpec.scala @@ -106,6 +106,37 @@ object SchemaWriterSpec extends SnapshotTest { isEffectTypeAbstract = true ) ), + snapshotTest("interface, abstracted effect and lazy combination")( + gen( + """ + directive @lazy on FIELD_DEFINITION + + type Query { + t0: T0! + t1: T1! + } + interface Direct { + i: Int! @lazy + } + + interface Transitive { + i: T2 + } + + type T0 implements Direct { + i: Int! @lazy + } + type T1 implements Transitive { + i: T2 + } + type T2 { + i: Int @lazy + } + """, + effect = "F", + isEffectTypeAbstract = true + ) + ), snapshotTest("simple mutation with abstracted effect type")( gen( """