Skip to content

Commit

Permalink
Add support for distinguishing explicit and implicit null in ArgBuild…
Browse files Browse the repository at this point in the history
…ers (#929)

* Add support for distinguishing explicit and implicit null in ArgBuilders

* Add scaladoc

* Add tests

* Fix lint issues

* Make Scala 3 happy

* Fix more lint issues

* Move test

* Remove left over imports
  • Loading branch information
stephendavidmarsh authored Jun 17, 2021
1 parent ec6d0bd commit 6ac0b94
Show file tree
Hide file tree
Showing 4 changed files with 44 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ trait ArgBuilderDerivation {
input match {
case InputValue.ObjectValue(fields) =>
val label = p.annotations.collectFirst { case GQLName(name) => name }.getOrElse(p.label)
p.typeclass.build(fields.getOrElse(label, NullValue))
fields.get(label).fold(p.typeclass.buildMissing)(p.typeclass.build)
case value => p.typeclass.build(value)
}
}
Expand Down
14 changes: 6 additions & 8 deletions core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,12 @@ trait ArgBuilderDerivation {
new ArgBuilder[A] {
def build(input: InputValue): Either[ExecutionError, A] = {
fields.map { (label, _, builder) =>
val newInput =
input match {
case InputValue.ObjectValue(fields) =>
val finalLabel = annotations.getOrElse(label, Nil).collectFirst { case GQLName(name) => name }.getOrElse(label)
fields.getOrElse(finalLabel, NullValue)
case value => value
}
builder.build(newInput)
input match {
case InputValue.ObjectValue(fields) =>
val finalLabel = annotations.getOrElse(label, Nil).collectFirst { case GQLName(name) => name }.getOrElse(label)
fields.get(finalLabel).fold(builder.buildMissing)(builder.build)
case value => builder.build(value)
}
}.foldRight[Either[ExecutionError, Tuple]](Right(EmptyTuple)) { case (item, acc) =>
item match {
case error: Left[ExecutionError, Any] => error.asInstanceOf[Left[ExecutionError, Tuple]]
Expand Down
7 changes: 7 additions & 0 deletions core/src/main/scala/caliban/schema/ArgBuilder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ trait ArgBuilder[T] { self =>
*/
def build(input: InputValue): Either[ExecutionError, T]

/**
* Builds a value of type `T` from a missing input value.
* By default, this delegates to [[build]], passing it NullValue.
* Fails with an [[caliban.CalibanError.ExecutionError]] if it was impossible to build the value.
*/
def buildMissing: Either[ExecutionError, T] = build(NullValue)

/**
* Builds a new `ArgBuilder` of `A` from an existing `ArgBuilder` of `T` and a function from `T` to `A`.
* @param f a function from `T` to `A`.
Expand Down
32 changes: 30 additions & 2 deletions core/src/test/scala/caliban/schema/ArgBuilderSpec.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package caliban.schema

import java.time.{ Instant, LocalDate, LocalDateTime, OffsetDateTime, OffsetTime, ZoneOffset, ZonedDateTime }

import caliban.Value.{ IntValue, StringValue }
import caliban.Value.{ IntValue, NullValue, StringValue }
import zio.test._
import Assertion._
import caliban.CalibanError.ExecutionError
import caliban.InputValue
import caliban.InputValue.ObjectValue

object ArgBuilderSpec extends DefaultRunnableSpec {
def spec = suite("ArgBuilder")(
Expand Down Expand Up @@ -56,6 +58,32 @@ object ArgBuilderSpec extends DefaultRunnableSpec {
isRight(equalTo(ZonedDateTime.ofInstant(Instant.ofEpochMilli(100), ZoneOffset.UTC)))
)
)
),
suite("buildMissing")(
test("works with derived case class ArgBuilders") {
sealed abstract class Nullable[+T]
case class SomeNullable[+T](t: T) extends Nullable[T]
case object NullNullable extends Nullable[Nothing]
case object MissingNullable extends Nullable[Nothing]

implicit def nullableArgBuilder[A](implicit ev: ArgBuilder[A]): ArgBuilder[Nullable[A]] =
new ArgBuilder[Nullable[A]] {
def build(input: InputValue): Either[ExecutionError, Nullable[A]] = input match {
case NullValue => Right(NullNullable)
case _ => ev.build(input).map(SomeNullable(_))
}

override def buildMissing: Either[ExecutionError, Nullable[A]] = Right(MissingNullable)
}

case class Wrapper(a: Nullable[String])

val deriviedAB = implicitly[ArgBuilder[Wrapper]]

assert(deriviedAB.build(ObjectValue(Map())))(equalTo(Right(Wrapper(MissingNullable)))) &&
assert(deriviedAB.build(ObjectValue(Map("a" -> NullValue))))(equalTo(Right(Wrapper(NullNullable)))) &&
assert(deriviedAB.build(ObjectValue(Map("a" -> StringValue("x")))))(equalTo(Right(Wrapper(SomeNullable("x")))))
}
)
)
}

0 comments on commit 6ac0b94

Please sign in to comment.