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

Fix oneOf when multiple inputs have the same default #2403

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
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
38 changes: 19 additions & 19 deletions core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,9 @@ trait CommonArgBuilderDerivation {
arr
}

private val required = params.collect { case (label, default) if default.isLeft => label }
override private[schema] def firstField: Option[String] = params.headOption.map(_._1)

override private[schema] val partial: PartialFunction[InputValue, Either[ExecutionError, T]] = {
case InputValue.ObjectValue(fields) if required.forall(fields.contains) => fromFields(fields)
}

def build(input: InputValue): Either[ExecutionError, T] =
override def build(input: InputValue): Either[ExecutionError, T] =
input match {
case InputValue.ObjectValue(fields) => fromFields(fields)
case value => ctx.constructMonadic(p => p.typeclass.build(value))
Expand Down Expand Up @@ -82,21 +78,25 @@ trait CommonArgBuilderDerivation {

private def makeOneOfBuilder[A](ctx: SealedTrait[ArgBuilder, A]): ArgBuilder[A] = new ArgBuilder[A] {

private def inputError(input: InputValue) =
ExecutionError(s"Invalid oneOf input $input for trait ${ctx.typeName.short}")

override val partial: PartialFunction[InputValue, Either[ExecutionError, A]] = {
val xs = ctx.subtypes.map(_.typeclass).toList.asInstanceOf[List[ArgBuilder[A]]]

val checkSize: PartialFunction[InputValue, Either[ExecutionError, A]] = {
case InputValue.ObjectValue(f) if f.size != 1 =>
Left(ExecutionError("Exactly one key must be specified for oneOf inputs"))
}
xs.foldLeft(checkSize)(_ orElse _.partial)
}
private val builders = ctx.subtypes
.map(_.typeclass.asInstanceOf[ArgBuilder[A]]) // asInstanceOf needed for 2.12
.flatMap(builder => builder.firstField.map((_, builder)))
.toMap

def build(input: InputValue): Either[ExecutionError, A] =
partial.applyOrElse(input, (in: InputValue) => Left(inputError(in)))
input match {
case InputValue.ObjectValue(fields) =>
if (fields.size != 1) {
Left(ExecutionError("Exactly one key must be specified for oneOf inputs"))
} else {
val (field, _) = fields.head
builders.get(field) match {
case None => Left(ExecutionError(s"Invalid oneOf input key $field for trait ${ctx.typeName.short}"))
case Some(builder) => builder.build(input)
}
}
case value => Left(ExecutionError(s"Invalid oneOf input $value for trait ${ctx.typeName.short}"))
}
}

}
Expand Down
36 changes: 20 additions & 16 deletions core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -105,18 +105,26 @@ trait CommonArgBuilderDerivation {
traitLabel: String
): ArgBuilder[A] = new ArgBuilder[A] {

override val partial: PartialFunction[InputValue, Either[ExecutionError, A]] = {
val xs = _subTypes.map(_._3).asInstanceOf[List[ArgBuilder[A]]]

val checkSize: PartialFunction[InputValue, Either[ExecutionError, A]] = {
case InputValue.ObjectValue(f) if f.size != 1 =>
Left(ExecutionError("Exactly one key must be specified for oneOf inputs"))
}
xs.foldLeft(checkSize)(_ orElse _.partial)
}
private val builders = _subTypes
.map(_._3)
.asInstanceOf[List[ArgBuilder[A]]]
.flatMap(builder => builder.firstField.map((_, builder)))
.toMap

def build(input: InputValue): Either[ExecutionError, A] =
partial.applyOrElse(input, (in: InputValue) => Left(inputError(in)))
input match {
case InputValue.ObjectValue(fields) =>
if (fields.size != 1) {
Left(ExecutionError("Exactly one key must be specified for oneOf inputs"))
} else {
val (field, _) = fields.head
builders.get(field) match {
case None => Left(ExecutionError(s"Invalid oneOf input key $field for trait $traitLabel"))
case Some(builder) => builder.build(input)
}
}
case value => Left(ExecutionError(s"Invalid oneOf input $value for trait $traitLabel"))
}

private def inputError(input: InputValue) =
ExecutionError(s"Invalid oneOf input $input for trait $traitLabel")
Expand All @@ -134,13 +142,9 @@ trait CommonArgBuilderDerivation {
(finalLabel, default, builder)
})

private val required = params.collect { case (label, default, _) if default.isLeft => label }

override private[schema] val partial: PartialFunction[InputValue, Either[ExecutionError, A]] = {
case InputValue.ObjectValue(fields) if required.forall(fields.contains) => fromFields(fields)
}
override private[schema] def firstField: Option[String] = params.headOption.map(_._1)

def build(input: InputValue): Either[ExecutionError, A] =
override def build(input: InputValue): Either[ExecutionError, A] =
input match {
case InputValue.ObjectValue(fields) => fromFields(fields)
case value => fromValue(value)
Expand Down
16 changes: 2 additions & 14 deletions core/src/main/scala/caliban/schema/ArgBuilder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,26 +29,14 @@ See https://ghostdogpr.github.io/caliban/docs/schema.html for more information.
)
trait ArgBuilder[T] { self =>

private[schema] def firstField: Option[String] = None

/**
* Builds a value of type `T` from an input [[caliban.InputValue]].
* Fails with an [[caliban.CalibanError.ExecutionError]] if it was impossible to build the value.
*/
def build(input: InputValue): Either[ExecutionError, T]

private[schema] def partial: PartialFunction[InputValue, Either[ExecutionError, T]] =
new PartialFunction[InputValue, Either[ExecutionError, T]] {
final def isDefinedAt(x: InputValue): Boolean = build(x).isRight
final def apply(x: InputValue): Either[ExecutionError, T] = build(x)

final override def applyOrElse[A1 <: InputValue, B1 >: Either[ExecutionError, T]](
x: A1,
default: A1 => B1
): B1 = {
val maybeMatch = build(x)
if (maybeMatch.isRight) maybeMatch else default(x)
}
}

/**
* Builds a value of type `T` from a missing input value.
* By default, this delegates to [[build]], passing it NullValue.
Expand Down
91 changes: 90 additions & 1 deletion core/src/test/scala/caliban/execution/ExecutionSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1505,8 +1505,97 @@ object ExecutionSpec extends ZIOSpecDefault {
ZIO.foldLeft(cases)(assertCompletes) { case (acc, (query, expected)) =>
api.interpreter
.flatMap(_.execute(query, variables = Map("args" -> ObjectValue(Map("intValue" -> IntValue(42))))))
.map(response => assertTrue(response.data.toString == expected))
.map(response => acc && assertTrue(response.data.toString == expected))
}
},
test("oneOf input with input object with all optional fields") {

case class AddPet(pet: Pet.Wrapper)
case class Queries(addPet: AddPet => Pet)

val api: GraphQL[Any] = graphQL(
RootResolver(
Queries(_.pet.pet)
)
)

val cases = List(
gqldoc("""{
addPet(pet: { cat: { name: "a" } }) {
__typename
... on Cat { name }
... on Dog { name }
}
}""") -> """{"addPet":{"__typename":"Cat","name":"a"}}""",
gqldoc("""{
addPet(pet: { dog: {} }) {
__typename
... on Cat { name }
... on Dog { name }
}
}""") -> """{"addPet":{"__typename":"Dog","name":null}}""",
gqldoc("""{
addPet(pet: { dog: { name: "b" } }) {
__typename
... on Cat { name }
... on Dog { name }
}
}""") -> """{"addPet":{"__typename":"Dog","name":"b"}}"""
)

ZIO.foldLeft(cases)(assertCompletes) { case (acc, (query, expected)) =>
api.interpreter
.flatMap(_.execute(query))
.map(response => acc && assertTrue(response.data.toString == expected))
}
}
)
}

// needs to be outside for Scala 2
sealed trait Pet
object Pet { parent =>

@GQLOneOfInput
@GQLName("Pet")
sealed trait Wrapper {
def pet: Pet
}
object Wrapper {
implicit val argBuilder: ArgBuilder[Wrapper] = ArgBuilder.gen
implicit val schema: Schema[Any, Wrapper] = Schema.gen
}

case class Cat(name: Option[String], numberOfLives: Option[Int]) extends Pet
object Cat {
@GQLName("Cat")
case class Wrapper(cat: Cat) extends parent.Wrapper {
override val pet = cat
}
object Wrapper {
implicit val argBuilder: ArgBuilder[Wrapper] = ArgBuilder.gen
implicit val schema: Schema[Any, Wrapper] = Schema.gen
}

implicit val argBuilder: ArgBuilder[Cat] = ArgBuilder.gen
implicit val schema: Schema[Any, Cat] = Schema.gen
}

case class Dog(name: Option[String], wagsTail: Option[Boolean]) extends Pet
object Dog {
@GQLName("Dog")
case class Wrapper(dog: Dog) extends parent.Wrapper {
override val pet = dog
}
object Wrapper {
implicit val argBuilder: ArgBuilder[Wrapper] = ArgBuilder.gen
implicit val schema: Schema[Any, Wrapper] = Schema.gen
}

implicit val argBuilder: ArgBuilder[Dog] = ArgBuilder.gen
implicit val schema: Schema[Any, Dog] = Schema.gen
}

implicit val argBuilder: ArgBuilder[Pet] = ArgBuilder.gen
implicit val schema: Schema[Any, Pet] = Schema.gen
}