diff --git a/tools/src/main/scala/caliban/tools/ClientWriter.scala b/tools/src/main/scala/caliban/tools/ClientWriter.scala index 8e98c1c78..89fe4e0cf 100644 --- a/tools/src/main/scala/caliban/tools/ClientWriter.scala +++ b/tools/src/main/scala/caliban/tools/ClientWriter.scala @@ -88,6 +88,15 @@ object ClientWriter { safeTypeName(name) -> op }.toMap + val knownInterfaceTypes = typesMap.collect { case (key, _: InterfaceTypeDefinition) => key } + val knownUnionTypes = typesMap.collect { case (key, _: UnionTypeDefinition) => key } + + def isOptionalInterfaceType(field: FieldDefinition): Boolean = + knownInterfaceTypes.exists(_.compareToIgnoreCase(Type.innerType(field.ofType)) == 0) + + def isOptionalUnionType(field: FieldDefinition): Boolean = + knownUnionTypes.exists(_.compareToIgnoreCase(Type.innerType(field.ofType)) == 0) + def writeFieldInfo(fieldInfo: FieldInfo): String = { val FieldInfo( name, @@ -309,16 +318,36 @@ object ClientWriter { def writeRootQuery(typedef: ObjectTypeDefinition): String = s"""object ${typedef.name} { - | ${typedef.fields - .map( - writeField( - _, - "_root_.caliban.client.Operations.RootQuery", - optionalUnion = false, - optionalInterface = false, - commonInterface = false + | ${typedef.fields.flatMap { field => + if (isOptionalInterfaceType(field)) { + Vector( + writeField( + field, + "_root_.caliban.client.Operations.RootQuery", + optionalUnion = false, + optionalInterface = false, + commonInterface = false + ), + writeField( + field, + "_root_.caliban.client.Operations.RootQuery", + optionalUnion = false, + optionalInterface = true, + commonInterface = false + ) ) - ) + } else { + Vector( + writeField( + field, + "_root_.caliban.client.Operations.RootQuery", + optionalUnion = false, + optionalInterface = false, + commonInterface = false + ) + ) + } + } .mkString("\n ")} |} |""".stripMargin @@ -330,16 +359,36 @@ object ClientWriter { def writeRootMutation(typedef: ObjectTypeDefinition): String = s"""object ${typedef.name} { - | ${typedef.fields - .map( - writeField( - _, - "_root_.caliban.client.Operations.RootMutation", - optionalUnion = false, - optionalInterface = false, - commonInterface = false + | ${typedef.fields.flatMap { field => + if (isOptionalInterfaceType(field)) { + Vector( + writeField( + field, + "_root_.caliban.client.Operations.RootMutation", + optionalUnion = false, + optionalInterface = false, + commonInterface = false + ), + writeField( + field, + "_root_.caliban.client.Operations.RootMutation", + optionalUnion = false, + optionalInterface = true, + commonInterface = false + ) ) - ) + } else { + Vector( + writeField( + field, + "_root_.caliban.client.Operations.RootMutation", + optionalUnion = false, + optionalInterface = false, + commonInterface = false + ) + ) + } + } .mkString("\n ")} |} |""".stripMargin @@ -351,16 +400,36 @@ object ClientWriter { def writeRootSubscription(typedef: ObjectTypeDefinition): String = s"""object ${typedef.name} { - | ${typedef.fields - .map( - writeField( - _, - "_root_.caliban.client.Operations.RootSubscription", - optionalUnion = false, - optionalInterface = false, - commonInterface = false + | ${typedef.fields.flatMap { field => + if (isOptionalInterfaceType(field)) { + Vector( + writeField( + field, + "_root_.caliban.client.Operations.RootSubscription", + optionalUnion = false, + optionalInterface = false, + commonInterface = false + ), + writeField( + field, + "_root_.caliban.client.Operations.RootSubscription", + optionalUnion = false, + optionalInterface = true, + commonInterface = false + ) ) - ) + } else { + Vector( + writeField( + field, + "_root_.caliban.client.Operations.RootSubscription", + optionalUnion = false, + optionalInterface = false, + commonInterface = false + ) + ) + } + } .mkString("\n ")} |} |""".stripMargin @@ -376,10 +445,8 @@ object ClientWriter { val objectName: String = safeTypeName(typedef.name) - val unionTypes = typesMap.collect { case (key, _: UnionTypeDefinition) => key } val optionalUnionTypeFields = typedef.fields.flatMap { field => - val isOptionalUnionType = unionTypes.exists(_.compareToIgnoreCase(Type.innerType(field.ofType)) == 0) - if (isOptionalUnionType) + if (isOptionalUnionType(field)) Some( collectFieldInfo( field, @@ -392,10 +459,8 @@ object ClientWriter { else None } - val interfaceTypes = typesMap.collect { case (key, _: InterfaceTypeDefinition) => key } val optionalInterfaceTypeFields = typedef.fields.flatMap { field => - val isOptionalInterfaceType = interfaceTypes.exists(_.compareToIgnoreCase(Type.innerType(field.ofType)) == 0) - if (isOptionalInterfaceType) + if (isOptionalInterfaceType(field)) Vector( collectFieldInfo( field, diff --git a/tools/src/test/scala/caliban/tools/ClientWriterViewSpec.scala b/tools/src/test/scala/caliban/tools/ClientWriterViewSpec.scala index 18b0fd6c5..80d01b56d 100644 --- a/tools/src/test/scala/caliban/tools/ClientWriterViewSpec.scala +++ b/tools/src/test/scala/caliban/tools/ClientWriterViewSpec.scala @@ -1152,6 +1152,182 @@ object Client { """ ) ) + }, + testM("root schema optional interface") { + val schema = + """ + schema { + query: Queries + mutation: Mutations + subscription: Subscriptions + } + + type Queries { + node(id: ID!): Node + } + + type Mutations { + updateNode(id: ID!, name: String): Node + } + + type Subscriptions { + node(id: ID!): Node + } + + interface Node { + id: ID! + } + type NodeA implements Node { + id: ID! + a: String + } + type NodeB implements Node { + id: ID! + b: Int + } + """ + + assertM(gen(schema))( + equalTo("""import caliban.client.FieldBuilder._ +import caliban.client._ + +object Client { + + type Node + object Node { + + final case class NodeView(id: String) + + type ViewSelection = SelectionBuilder[Node, NodeView] + + def view: ViewSelection = id.map(id => NodeView(id)) + + def id: SelectionBuilder[Node, String] = _root_.caliban.client.SelectionBuilder.Field("id", Scalar()) + } + + type NodeA + object NodeA { + + final case class NodeAView(id: String, a: Option[String]) + + type ViewSelection = SelectionBuilder[NodeA, NodeAView] + + def view: ViewSelection = (id ~ a).map { case (id, a) => NodeAView(id, a) } + + def id: SelectionBuilder[NodeA, String] = _root_.caliban.client.SelectionBuilder.Field("id", Scalar()) + def a: SelectionBuilder[NodeA, Option[String]] = + _root_.caliban.client.SelectionBuilder.Field("a", OptionOf(Scalar())) + } + + type NodeB + object NodeB { + + final case class NodeBView(id: String, b: Option[Int]) + + type ViewSelection = SelectionBuilder[NodeB, NodeBView] + + def view: ViewSelection = (id ~ b).map { case (id, b) => NodeBView(id, b) } + + def id: SelectionBuilder[NodeB, String] = _root_.caliban.client.SelectionBuilder.Field("id", Scalar()) + def b: SelectionBuilder[NodeB, Option[Int]] = _root_.caliban.client.SelectionBuilder.Field("b", OptionOf(Scalar())) + } + + type Queries = _root_.caliban.client.Operations.RootQuery + object Queries { + def node[A](id: String)(onNodeA: SelectionBuilder[NodeA, A], onNodeB: SelectionBuilder[NodeB, A])(implicit + encoder0: ArgEncoder[String] + ): SelectionBuilder[_root_.caliban.client.Operations.RootQuery, Option[A]] = + _root_.caliban.client.SelectionBuilder.Field( + "node", + OptionOf(ChoiceOf(Map("NodeA" -> Obj(onNodeA), "NodeB" -> Obj(onNodeB)))), + arguments = List(Argument("id", id, "ID!")(encoder0)) + ) + def nodeOption[A]( + id: String + )(onNodeA: Option[SelectionBuilder[NodeA, A]] = None, onNodeB: Option[SelectionBuilder[NodeB, A]] = None)(implicit + encoder0: ArgEncoder[String] + ): SelectionBuilder[_root_.caliban.client.Operations.RootQuery, Option[Option[A]]] = + _root_.caliban.client.SelectionBuilder.Field( + "node", + OptionOf( + ChoiceOf( + Map( + "NodeA" -> onNodeA.fold[FieldBuilder[Option[A]]](NullField)(a => OptionOf(Obj(a))), + "NodeB" -> onNodeB.fold[FieldBuilder[Option[A]]](NullField)(a => OptionOf(Obj(a))) + ) + ) + ), + arguments = List(Argument("id", id, "ID!")(encoder0)) + ) + } + + type Mutations = _root_.caliban.client.Operations.RootMutation + object Mutations { + def updateNode[A]( + id: String, + name: Option[String] = None + )(onNodeA: SelectionBuilder[NodeA, A], onNodeB: SelectionBuilder[NodeB, A])(implicit + encoder0: ArgEncoder[String], + encoder1: ArgEncoder[Option[String]] + ): SelectionBuilder[_root_.caliban.client.Operations.RootMutation, Option[A]] = + _root_.caliban.client.SelectionBuilder.Field( + "updateNode", + OptionOf(ChoiceOf(Map("NodeA" -> Obj(onNodeA), "NodeB" -> Obj(onNodeB)))), + arguments = List(Argument("id", id, "ID!")(encoder0), Argument("name", name, "String")(encoder1)) + ) + def updateNodeOption[A]( + id: String, + name: Option[String] = None + )(onNodeA: Option[SelectionBuilder[NodeA, A]] = None, onNodeB: Option[SelectionBuilder[NodeB, A]] = None)(implicit + encoder0: ArgEncoder[String], + encoder1: ArgEncoder[Option[String]] + ): SelectionBuilder[_root_.caliban.client.Operations.RootMutation, Option[Option[A]]] = + _root_.caliban.client.SelectionBuilder.Field( + "updateNode", + OptionOf( + ChoiceOf( + Map( + "NodeA" -> onNodeA.fold[FieldBuilder[Option[A]]](NullField)(a => OptionOf(Obj(a))), + "NodeB" -> onNodeB.fold[FieldBuilder[Option[A]]](NullField)(a => OptionOf(Obj(a))) + ) + ) + ), + arguments = List(Argument("id", id, "ID!")(encoder0), Argument("name", name, "String")(encoder1)) + ) + } + + type Subscriptions = _root_.caliban.client.Operations.RootSubscription + object Subscriptions { + def node[A](id: String)(onNodeA: SelectionBuilder[NodeA, A], onNodeB: SelectionBuilder[NodeB, A])(implicit + encoder0: ArgEncoder[String] + ): SelectionBuilder[_root_.caliban.client.Operations.RootSubscription, Option[A]] = + _root_.caliban.client.SelectionBuilder.Field( + "node", + OptionOf(ChoiceOf(Map("NodeA" -> Obj(onNodeA), "NodeB" -> Obj(onNodeB)))), + arguments = List(Argument("id", id, "ID!")(encoder0)) + ) + def nodeOption[A]( + id: String + )(onNodeA: Option[SelectionBuilder[NodeA, A]] = None, onNodeB: Option[SelectionBuilder[NodeB, A]] = None)(implicit + encoder0: ArgEncoder[String] + ): SelectionBuilder[_root_.caliban.client.Operations.RootSubscription, Option[Option[A]]] = + _root_.caliban.client.SelectionBuilder.Field( + "node", + OptionOf( + ChoiceOf( + Map( + "NodeA" -> onNodeA.fold[FieldBuilder[Option[A]]](NullField)(a => OptionOf(Obj(a))), + "NodeB" -> onNodeB.fold[FieldBuilder[Option[A]]](NullField)(a => OptionOf(Obj(a))) + ) + ) + ), + arguments = List(Argument("id", id, "ID!")(encoder0)) + ) + } + +} +""") + ) } ) @@ TestAspect.sequential }