caliban.schema.ArgBuilder.scala Maven / Gradle / Ivy
The newest version!
package caliban.schema
import caliban.CalibanError.ExecutionError
import caliban.InputValue
import caliban.Value._
import caliban.parsing.Parser
import caliban.uploads.Upload
import zio.Chunk
import java.time._
import java.time.format.DateTimeFormatter
import java.time.temporal.Temporal
import java.util.UUID
import scala.annotation.implicitNotFound
import scala.util.Try
import scala.util.control.{ NoStackTrace, NonFatal }
/**
* Typeclass that defines how to build an argument of type `T` from an input [[caliban.InputValue]].
* Every type that can be passed as an argument needs an instance of `ArgBuilder`.
*/
@implicitNotFound(
"""Cannot find an ArgBuilder for type ${T}.
Caliban provides instances of ArgBuilder for the most common Scala types, and can derive it for your case classes and sealed traits.
Derivation requires that you have a Schema for any other type nested inside ${T}.
See https://ghostdogpr.github.io/caliban/docs/schema.html for more information.
"""
)
trait ArgBuilder[T] { self =>
/**
* 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.
* Fails with an [[caliban.CalibanError.ExecutionError]] if it was impossible to build the value.
*/
def buildMissing(default: Option[String]): Either[ExecutionError, T] =
default
.map(
Parser
.parseInputValue(_)
.flatMap(build)
.left
.map(e => ExecutionError(e.getMessage(), innerThrowable = Some(InvalidInputArgument)))
)
.getOrElse(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`.
*/
def map[A](f: T => A): ArgBuilder[A] = (input: InputValue) => self.build(input).map(f)
/**
* Builds a new `ArgBuilder` of A from an existing `ArgBuilder` of `T` and a function from `T` to `Either[ExecutionError, A]`.
* @param f a function from `T` to Either[ExecutionError, A]
*/
def flatMap[A](f: T => Either[ExecutionError, A]): ArgBuilder[A] = (input: InputValue) => self.build(input).flatMap(f)
/**
* Builds a new `ArgBuilder` of T from two `ArgBuilders` of `T` where the second `ArgBuilder` is a fallback if the first one fails
* In the case that both fail, the error from the second will be returned
* @param fallback The alternative `ArgBuilder` if this one fails
*/
def orElse(fallback: ArgBuilder[T]): ArgBuilder[T] =
(input: InputValue) =>
self.build(input) match {
case Left(_) => fallback.build(input)
case pass => pass
}
/**
* @see [[orElse]]
*/
def ||(fallback: ArgBuilder[T]): ArgBuilder[T] =
orElse(fallback)
}
object ArgBuilder extends ArgBuilderInstances {
def apply[T](implicit ev: ArgBuilder[T]): ArgBuilder[T] = ev
object auto extends AutoArgBuilderDerivation
}
trait ArgBuilderInstances extends ArgBuilderDerivation {
implicit lazy val unit: ArgBuilder[Unit] = _ => Right(())
implicit lazy val int: ArgBuilder[Int] = {
case value: IntValue => Right(value.toInt)
case other => Left(InvalidInputArgument("Int", other))
}
implicit lazy val long: ArgBuilder[Long] = {
case value: IntValue => Right(value.toLong)
case StringValue(value) =>
Try(value.toLong).fold(_ => Left(InvalidInputArgument("Long", value)), Right(_))
case other => Left(InvalidInputArgument("Long", other))
}
implicit lazy val bigInt: ArgBuilder[BigInt] = {
case value: IntValue => Right(value.toBigInt)
case other => Left(InvalidInputArgument("BigInt", other))
}
implicit lazy val float: ArgBuilder[Float] = {
case value: IntValue => Right(value.toLong.toFloat)
case value: FloatValue => Right(value.toFloat)
case other => Left(InvalidInputArgument("Float", other))
}
implicit lazy val double: ArgBuilder[Double] = {
case value: IntValue => Right(value.toLong.toDouble)
case value: FloatValue => Right(value.toDouble)
case other => Left(InvalidInputArgument("Double", other))
}
implicit lazy val bigDecimal: ArgBuilder[BigDecimal] = {
case value: IntValue => Right(BigDecimal(value.toBigInt))
case value: FloatValue => Right(value.toBigDecimal)
case other => Left(InvalidInputArgument("BigDecimal", other))
}
implicit lazy val string: ArgBuilder[String] = {
case StringValue(value) => Right(value)
case other => Left(InvalidInputArgument("String", other))
}
implicit lazy val uuid: ArgBuilder[UUID] = {
case StringValue(value) =>
Try(UUID.fromString(value)).fold(_ => Left(InvalidInputArgument("UUID", value)), Right(_))
case other => Left(InvalidInputArgument("UUID", other))
}
implicit lazy val boolean: ArgBuilder[Boolean] = {
case BooleanValue(value) => Right(value)
case other => Left(InvalidInputArgument("Boolean", other))
}
private abstract class TemporalDecoder[A](name: String) extends ArgBuilder[A] {
protected[this] def parseUnsafe(input: String): A
override def build(input: InputValue): Either[ExecutionError, A] = input match {
case StringValue(value) =>
try Right(parseUnsafe(value))
catch {
case NonFatal(e) =>
val message = e.getMessage
if (message.eq(null)) Left(InvalidInputArgument(name, value))
else Left(InvalidInputArgument(name, s"$value ($message)"))
}
case _ =>
Left(InvalidInputArgument(name, input))
}
}
private object TemporalDecoder {
def apply[T <: Temporal](name: String)(parse: String => T): TemporalDecoder[T] = new TemporalDecoder[T](name) {
override protected[this] def parseUnsafe(input: String): T = parse(input)
}
}
final def localDateWithFormatter(formatter: DateTimeFormatter): ArgBuilder[LocalDate] =
TemporalDecoder("LocalDate")(LocalDate.parse(_, formatter))
final def localTimeWithFormatter(formatter: DateTimeFormatter): ArgBuilder[LocalTime] =
TemporalDecoder("LocalTime")(LocalTime.parse(_, formatter))
final def localDateTimeWithFormatter(formatter: DateTimeFormatter): ArgBuilder[LocalDateTime] =
TemporalDecoder("LocalDateTime")(LocalDateTime.parse(_, formatter))
final def offsetTimeWithFormatter(formatter: DateTimeFormatter): ArgBuilder[OffsetTime] =
TemporalDecoder("OffsetTime")(OffsetTime.parse(_, formatter))
final def offsetDateTimeWithFormatter(formatter: DateTimeFormatter): ArgBuilder[OffsetDateTime] =
TemporalDecoder("OffsetDateTime")(OffsetDateTime.parse(_, formatter))
final def zonedDateTimeWithFormatter(formatter: DateTimeFormatter): ArgBuilder[ZonedDateTime] =
TemporalDecoder("ZonedDateTime")(ZonedDateTime.parse(_, formatter))
lazy val instantEpoch: ArgBuilder[Instant] = {
case i: IntValue => Right(Instant.ofEpochMilli(i.toLong))
case value => Left(InvalidInputArgument("Instant", value))
}
implicit lazy val instant: ArgBuilder[Instant] = TemporalDecoder("Instant")(Instant.parse)
implicit lazy val localDate: ArgBuilder[LocalDate] = TemporalDecoder("LocalDate")(LocalDate.parse)
implicit lazy val localTime: ArgBuilder[LocalTime] = TemporalDecoder("LocalTime")(LocalTime.parse)
implicit lazy val localDateTime: ArgBuilder[LocalDateTime] = TemporalDecoder("LocalDateTime")(LocalDateTime.parse)
implicit lazy val offsetTime: ArgBuilder[OffsetTime] = TemporalDecoder("OffsetTime")(OffsetTime.parse)
implicit lazy val zonedDateTime: ArgBuilder[ZonedDateTime] = TemporalDecoder("ZonedDateTime")(ZonedDateTime.parse)
implicit lazy val offsetDateTime: ArgBuilder[OffsetDateTime] = TemporalDecoder("OffsetDateTime")(OffsetDateTime.parse)
implicit def option[A](implicit ev: ArgBuilder[A]): ArgBuilder[Option[A]] = {
case NullValue => Right(None)
case value => ev.build(value).map(Some(_))
}
implicit def list[A](implicit ev: ArgBuilder[A]): ArgBuilder[List[A]] = {
case InputValue.ListValue(items) =>
items
.foldLeft[Either[ExecutionError, List[A]]](Right(Nil)) {
case (res @ Left(_), _) => res
case (Right(res), value) =>
ev.build(value) match {
case Left(error) => Left(error)
case Right(value) => Right(value :: res)
}
}
.map(_.reverse)
case other => ev.build(other).map(List(_))
}
implicit def seq[A](implicit ev: ArgBuilder[A]): ArgBuilder[Seq[A]] = list[A].map(_.toSeq)
implicit def set[A](implicit ev: ArgBuilder[A]): ArgBuilder[Set[A]] = list[A].map(_.toSet)
implicit def vector[A](implicit ev: ArgBuilder[A]): ArgBuilder[Vector[A]] = list[A].map(_.toVector)
implicit def chunk[A](implicit ev: ArgBuilder[A]): ArgBuilder[Chunk[A]] = list[A].map(Chunk.fromIterable)
implicit lazy val upload: ArgBuilder[Upload] = {
case StringValue(v) => Right(Upload(v))
case other => Left(InvalidInputArgument("Upload", other))
}
}
case object InvalidInputArgument extends NoStackTrace {
override def getMessage: String = "invalid input argument"
private[caliban] def apply(argType: String, argValue: Any): ExecutionError =
ExecutionError(
s"Can't build an instance of '$argType' from '$argValue'",
innerThrowable = Some(InvalidInputArgument)
)
}