Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Partial union type query #446 #790

Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions client/src/main/scala/caliban/client/FieldBuilder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,21 @@ object FieldBuilder {
case _ => Left(DecodingError(s"Field $value is not an object"))
}

override def toSelectionSet: List[Selection] =
override def toSelectionSet: List[Selection] = {
val filteredBuilderMap = builderMap.filter((f) => !isNullField(f._2));
Selection.Field(None, "__typename", Nil, Nil, Nil, 0) ::
builderMap.map { case (k, v) => Selection.InlineFragment(k, v.toSelectionSet) }.toList
filteredBuilderMap.map { case (k, v) =>
Selection.InlineFragment(k, v.toSelectionSet)
}.toList
}
}
case object NullField extends FieldBuilder[Option[Nothing]] {
override def fromGraphQL(value: __Value): Either[DecodingError, Option[Nothing]] = Right(None)
override def toSelectionSet: List[Selection] = Nil
}

def isNullField(p: Any): Boolean = p match {
case NullField => true
case _ => false
}
}
67 changes: 65 additions & 2 deletions client/src/test/scala/caliban/client/SelectionBuilderSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ object SelectionBuilderSpec extends DefaultRunnableSpec {
Character.name ~
Character.nicknames ~
Character
.role(Role.Captain.shipName, Role.Pilot.shipName, Role.Mechanic.shipName, Role.Engineer.shipName)
.role(
onCaptain = Role.Captain.shipName,
onPilot = Role.Pilot.shipName,
onMechanic = Role.Mechanic.shipName,
onEngineer = Role.Engineer.shipName
)
}
val (s, _) = SelectionBuilder.toGraphQL(query.toSelectionSet, useVariables = false)
assert(s)(
Expand All @@ -44,6 +49,21 @@ object SelectionBuilderSpec extends DefaultRunnableSpec {
)
)
},
test("union type with optional parameters") {
val query =
Queries.characters() {
Character.name ~
Character.nicknames ~
Character
.roleOption(onEngineer = Some(Role.Engineer.shipName))
}
val (s, _) = SelectionBuilder.toGraphQL(query.toSelectionSet, useVariables = false)
assert(s)(
equalTo(
"characters{name nicknames role{__typename ... on Engineer{shipName}}}"
)
)
},
test("argument") {
val query =
Queries.characters(Some(Origin.MARS)) {
Expand Down Expand Up @@ -154,7 +174,12 @@ object SelectionBuilderSpec extends DefaultRunnableSpec {
(Character.name ~
Character.nicknames ~
Character
.role(Role.Captain.shipName, Role.Pilot.shipName, Role.Mechanic.shipName, Role.Engineer.shipName))
.role(
onCaptain = Role.Captain.shipName,
onPilot = Role.Pilot.shipName,
onMechanic = Role.Mechanic.shipName,
onEngineer = Role.Engineer.shipName
))
.mapN(CharacterView)
}

Expand Down Expand Up @@ -183,6 +208,44 @@ object SelectionBuilderSpec extends DefaultRunnableSpec {
isRight(equalTo(List(CharacterView("Amos Burton", List("Amos"), Some("Rocinante")))))
)
},
test("union type with optional parameters") {
case class CharacterView(name: String, nicknames: List[String], role: Option[Option[String]])
val query =
Queries.characters() {
(Character.name ~
Character.nicknames ~
Character
.roleOption(
onMechanic = Some(Role.Mechanic.shipName)
))
.mapN(CharacterView)
}

val response =
__ObjectValue(
List(
"characters" -> __ListValue(
List(
__ObjectValue(
List(
"name" -> __StringValue("Amos Burton"),
"nicknames" -> __ListValue(List(__StringValue("Amos"))),
"role" -> __ObjectValue(
List(
"__typename" -> __StringValue("Mechanic"),
"shipName" -> __StringValue("Rocinante")
)
)
)
)
)
)
)
)
assert(query.fromGraphQL(response))(
isRight(equalTo(List(CharacterView("Amos Burton", List("Amos"), Some(Some("Rocinante"))))))
)
},
test("aliases") {
val query =
Queries
Expand Down
19 changes: 19 additions & 0 deletions client/src/test/scala/caliban/client/TestData.scala
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,25 @@ object TestData {
)
)
)
def roleOption[A](
onCaptain: Option[SelectionBuilder[Captain, A]] = None,
onEngineer: Option[SelectionBuilder[Engineer, A]] = None,
onMechanic: Option[SelectionBuilder[Mechanic, A]] = None,
onPilot: Option[SelectionBuilder[Pilot, A]] = None
): SelectionBuilder[Character, Option[Option[A]]] =
Field(
"role",
OptionOf(
ChoiceOf(
Map(
"Captain" -> onCaptain.fold[FieldBuilder[Option[A]]](NullField)(a => OptionOf(Obj(a))),
"Engineer" -> onEngineer.fold[FieldBuilder[Option[A]]](NullField)(a => OptionOf(Obj(a))),
"Mechanic" -> onMechanic.fold[FieldBuilder[Option[A]]](NullField)(a => OptionOf(Obj(a))),
"Pilot" -> onPilot.fold[FieldBuilder[Option[A]]](NullField)(a => OptionOf(Obj(a)))
)
)
)
)
}

// Auto-generated query
Expand Down
19 changes: 19 additions & 0 deletions examples/src/main/scala/caliban/client/Client.scala
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,25 @@ object Client {
)
)
)
def roleOption[A](
onCaptain: Option[SelectionBuilder[Captain, A]] = None,
onEngineer: Option[SelectionBuilder[Engineer, A]] = None,
onMechanic: Option[SelectionBuilder[Mechanic, A]] = None,
onPilot: Option[SelectionBuilder[Pilot, A]] = None
): SelectionBuilder[Character, Option[Option[A]]] =
Field(
"role",
OptionOf(
ChoiceOf(
Map(
"Captain" -> onCaptain.fold[FieldBuilder[Option[A]]](NullField)(a => OptionOf(Obj(a))),
"Engineer" -> onEngineer.fold[FieldBuilder[Option[A]]](NullField)(a => OptionOf(Obj(a))),
"Mechanic" -> onMechanic.fold[FieldBuilder[Option[A]]](NullField)(a => OptionOf(Obj(a))),
"Pilot" -> onPilot.fold[FieldBuilder[Option[A]]](NullField)(a => OptionOf(Obj(a)))
)
)
)
)
}

type Pilot
Expand Down
100 changes: 57 additions & 43 deletions tools/src/main/scala/caliban/tools/ClientWriter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -83,27 +83,26 @@ object ClientWriter {
.map(t => writeRootSubscription(t, typesMap, mappingClashedTypeNames))
.getOrElse("")

val imports =
s"""${if (enums.nonEmpty)
"""import caliban.client.CalibanClientError.DecodingError
|""".stripMargin
else ""}${if (objects.nonEmpty || queries.nonEmpty || mutations.nonEmpty || subscriptions.nonEmpty)
"""import caliban.client.FieldBuilder._
|import caliban.client.SelectionBuilder._
|""".stripMargin
else
""}${if (
enums.nonEmpty || objects.nonEmpty || queries.nonEmpty || mutations.nonEmpty || subscriptions.nonEmpty || inputs.nonEmpty
)
"""import caliban.client._
|""".stripMargin
else ""}${if (queries.nonEmpty || mutations.nonEmpty || subscriptions.nonEmpty)
"""import caliban.client.Operations._
|""".stripMargin
else ""}${if (enums.nonEmpty || inputs.nonEmpty)
"""import caliban.client.__Value._
|""".stripMargin
else ""}"""
val imports = s"""${if (enums.nonEmpty)
"""import caliban.client.CalibanClientError.DecodingError
|""".stripMargin
else ""}${if (objects.nonEmpty || queries.nonEmpty || mutations.nonEmpty || subscriptions.nonEmpty)
"""import caliban.client.FieldBuilder._
|import caliban.client.SelectionBuilder._
|""".stripMargin
else
""}${if (
enums.nonEmpty || objects.nonEmpty || queries.nonEmpty || mutations.nonEmpty || subscriptions.nonEmpty || inputs.nonEmpty
)
"""import caliban.client._
|""".stripMargin
else ""}${if (queries.nonEmpty || mutations.nonEmpty || subscriptions.nonEmpty)
"""import caliban.client.Operations._
|""".stripMargin
else ""}${if (enums.nonEmpty || inputs.nonEmpty)
"""import caliban.client.__Value._
|""".stripMargin
else ""}"""

s"""${packageName.fold("")(p => s"package $p\n\n")}$imports
|
Expand All @@ -125,9 +124,10 @@ object ClientWriter {
.map(name => name.toLowerCase -> name)
.groupBy(_._1)
.collect {
case (_, _ :: typeNamesToRename) if typeNamesToRename.nonEmpty =>
typeNamesToRename.map { case (_, originalTypeName) =>
originalTypeName -> s"`$originalTypeName`"
case (_, (_ :: typeNamesToRename)) if typeNamesToRename.nonEmpty =>
typeNamesToRename.zipWithIndex.map { case (((_, originalTypeName), index)) =>
val suffix = "_" * (index + 1)
originalTypeName -> s"$originalTypeName$suffix"
}.toMap
}
.reduceOption(_ ++ _)
Expand Down Expand Up @@ -376,18 +376,15 @@ object ClientWriter {

def writeScalar(typedef: ScalarTypeDefinition, mappingClashedTypeNames: Map[String, String]): String =
if (typedef.name == "Json") "type Json = io.circe.Json"
else
s"""type ${safeTypeName(typedef.name, mappingClashedTypeNames)} = String
else s"""type ${safeTypeName(typedef.name, mappingClashedTypeNames)} = String
"""

def safeUnapplyName(name: String): String =
if (reservedKeywords.contains(name) || name.endsWith("_")) s"$name$$"
if (reservedKeywords.contains(name)) s"${name}_"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's a merge issue here, those changes should not be reverted and probably explain the CI error.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for spotting that. I will try to update that and let you know how it goes.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the same issue with:

[info] [error] /tmp/sbt_f9ee2458/src/main/scala/Client.scala:82:7: overriding method wait in class Object of type ()Unit;
[info] [error]  value wait cannot override final member
[info] [error]       wait: String
[info] [error]       ^

I will look over the merge conflicts that I attempted to resolve and see if I screwed other spots up too.

else name

def safeName(name: String): String =
if (reservedKeywords.contains(name) || name.endsWith("_")) s"`$name`"
else if (caseClassReservedFields.contains(name)) s"$name$$"
else name
if (reservedKeywords.contains(name)) s"`$name`" else name

@tailrec
def getTypeLetter(typesMap: Map[String, TypeDefinition], letter: String = "A"): String =
Expand All @@ -400,14 +397,16 @@ object ClientWriter {
field: FieldDefinition,
typeName: String,
typesMap: Map[String, TypeDefinition],
mappingClashedTypeNames: Map[String, String]
mappingClashedTypeNames: Map[String, String],
optionalType: Boolean = false
): String =
writeFieldInfo(collectFieldInfo(field, typeName, typesMap, mappingClashedTypeNames))
writeFieldInfo(collectFieldInfo(field, typeName, typesMap, mappingClashedTypeNames, optionalType))

def writeFieldInfo(fieldInfo: FieldInfo): String = {
val FieldInfo(
name,
safeName,
optionName,
description,
deprecated,
typeName,
Expand All @@ -421,16 +420,18 @@ object ClientWriter {
) =
fieldInfo

s"""$description${deprecated}def $safeName$typeParam$args$innerSelection: SelectionBuilder[$typeName, $outputType] = Field("$name", $builder$argBuilder)"""
s"""$description${deprecated}def $safeName${optionName}$typeParam$args$innerSelection: SelectionBuilder[$typeName, $outputType] = Field("$name", $builder$argBuilder)"""
}

def collectFieldInfo(
field: FieldDefinition,
typeName: String,
typesMap: Map[String, TypeDefinition],
mappingClashedTypeNames: Map[String, String]
mappingClashedTypeNames: Map[String, String],
optionalType: Boolean = false
): FieldInfo = {
val name = safeName(field.name)
val optionName = if (optionalType) "Option" else ""
val description = field.description match {
case Some(d) if d.trim.nonEmpty => s"/**\n * ${d.trim}\n */\n"
case _ => ""
Expand Down Expand Up @@ -488,15 +489,27 @@ object ClientWriter {
writeTypeBuilder(field.ofType, "Scalar()")
)
} else if (unionTypes.nonEmpty) {
(
s"[$typeLetter]",
s"(${unionTypes.map(t => s"""on${t.name}: SelectionBuilder[${safeTypeName(t.name, mappingClashedTypeNames)}, $typeLetter]""").mkString(", ")})",
writeType(field.ofType, mappingClashedTypeNames).replace(fieldType, typeLetter),
writeTypeBuilder(
field.ofType,
s"ChoiceOf(Map(${unionTypes.map(t => s""""${t.name}" -> Obj(on${t.name})""").mkString(", ")}))"
if (optionalType) {
(
s"[$typeLetter]",
s"(${unionTypes.map(t => s"""on${t.name}: Option[SelectionBuilder[${safeTypeName(t.name, mappingClashedTypeNames)}, $typeLetter]] = None""").mkString(", ")})",
s"Option[${writeType(field.ofType, mappingClashedTypeNames).replace(fieldType, typeLetter)}]",
writeTypeBuilder(
field.ofType,
s"ChoiceOf(Map(${unionTypes.map(t => s""""${t.name}" -> on${t.name}.fold[FieldBuilder[Option[A]]](NullField)(a => OptionOf(Obj(a)))""").mkString(", ")}))"
)
)
)
} else {
(
s"[$typeLetter]",
s"(${unionTypes.map(t => s"""on${t.name}: SelectionBuilder[${safeTypeName(t.name, mappingClashedTypeNames)}, $typeLetter]""").mkString(", ")})",
writeType(field.ofType, mappingClashedTypeNames).replace(fieldType, typeLetter),
writeTypeBuilder(
field.ofType,
s"ChoiceOf(Map(${unionTypes.map(t => s""""${t.name}" -> Obj(on${t.name})""").mkString(", ")}))"
)
)
}
} else if (interfaceTypes.nonEmpty) {
(
s"[$typeLetter]",
Expand Down Expand Up @@ -538,6 +551,7 @@ object ClientWriter {
FieldInfo(
field.name,
name,
optionName,
description,
deprecated,
typeName,
Expand Down Expand Up @@ -655,6 +669,7 @@ object ClientWriter {
final case class FieldInfo(
name: String,
safeName: String,
optionName: String,
description: String,
deprecated: String,
typeName: String,
Expand All @@ -666,5 +681,4 @@ object ClientWriter {
argBuilder: String,
typeInfo: FieldTypeInfo
)

}
15 changes: 15 additions & 0 deletions tools/src/test/scala/caliban/tools/ClientWriterSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,21 @@ object Client {
onPilot: SelectionBuilder[Pilot, A]
): SelectionBuilder[Character, Option[A]] =
Field("role", OptionOf(ChoiceOf(Map("Captain" -> Obj(onCaptain), "Pilot" -> Obj(onPilot)))))
def roleOption[A](
onCaptain: Option[SelectionBuilder[Captain, A]] = None,
onPilot: Option[SelectionBuilder[Pilot, A]] = None
): SelectionBuilder[Character, Option[Option[A]]] =
Field(
"role",
OptionOf(
ChoiceOf(
Map(
"Captain" -> onCaptain.fold[FieldBuilder[Option[A]]](NullField)(a => OptionOf(Obj(a))),
"Pilot" -> onPilot.fold[FieldBuilder[Option[A]]](NullField)(a => OptionOf(Obj(a)))
)
)
)
)
}

}
Expand Down
Loading