diff --git a/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala b/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala index 34324a5505..a6edbdcf1b 100644 --- a/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala @@ -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)) @@ -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}")) + } } } diff --git a/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala b/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala index 1160acfdc5..05c6a1ad52 100644 --- a/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala +++ b/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala @@ -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") @@ -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) diff --git a/core/src/main/scala/caliban/schema/ArgBuilder.scala b/core/src/main/scala/caliban/schema/ArgBuilder.scala index 985f97d213..52b8dc9bd2 100644 --- a/core/src/main/scala/caliban/schema/ArgBuilder.scala +++ b/core/src/main/scala/caliban/schema/ArgBuilder.scala @@ -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. diff --git a/core/src/test/scala/caliban/execution/ExecutionSpec.scala b/core/src/test/scala/caliban/execution/ExecutionSpec.scala index c8b328fb68..950f7be08d 100644 --- a/core/src/test/scala/caliban/execution/ExecutionSpec.scala +++ b/core/src/test/scala/caliban/execution/ExecutionSpec.scala @@ -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 +}