From 6b92ec8359ec9ea99e19d597ffdac9e359a4834f Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Sun, 20 Aug 2023 15:37:22 +0530 Subject: [PATCH 01/25] Add `ArgBuilder` derivation for `oneOff` inputs --- .../caliban/schema/ArgBuilderDerivation.scala | 24 +++++- .../caliban/schema/ArgBuilderDerivation.scala | 80 ++++++++++++++----- .../caliban/schema/SchemaDerivation.scala | 38 +++++---- .../caliban/schema/macros/Macros.scala | 27 ++++++- .../scala/caliban/schema/Annotations.scala | 5 ++ .../scala/caliban/schema/ArgBuilder.scala | 2 + .../scala/caliban/schema/ArgBuilderSpec.scala | 39 ++++++++- 7 files changed, 175 insertions(+), 40 deletions(-) diff --git a/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala b/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala index 8e1f8f8fb2..a6c1462023 100644 --- a/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala @@ -3,8 +3,7 @@ package caliban.schema import caliban.CalibanError.ExecutionError import caliban.InputValue import caliban.Value._ -import caliban.schema.Annotations.GQLDefault -import caliban.schema.Annotations.GQLName +import caliban.schema.Annotations.{ GQLDefault, GQLName, GQLOneOfInput } import magnolia._ import mercator.Monadic @@ -38,7 +37,11 @@ trait CommonArgBuilderDerivation { } } - def dispatch[T](ctx: SealedTrait[ArgBuilder, T]): ArgBuilder[T] = input => + def dispatch[T](ctx: SealedTrait[ArgBuilder, T]): ArgBuilder[T] = + if (ctx.annotations.contains(GQLOneOfInput())) makeOneOffBuilder(ctx) + else makeSumBuilder(ctx) + + private def makeSumBuilder[T](ctx: SealedTrait[ArgBuilder, T]): ArgBuilder[T] = input => (input match { case EnumValue(value) => Some(value) case StringValue(value) => Some(value) @@ -54,6 +57,21 @@ trait CommonArgBuilderDerivation { } case None => Left(ExecutionError(s"Can't build a trait from input $input")) } + + private def makeOneOffBuilder[A](ctx: SealedTrait[ArgBuilder, A]): ArgBuilder[A] = new ArgBuilder.OneOff[A] { + private lazy val builders = ctx.subtypes.map(_.typeclass) + + def build(input: InputValue): Either[ExecutionError, A] = input match { + case InputValue.ObjectValue(value) if value.size == 1 => + builders.view + .map(_.build(input)) + .find(_.isRight) + .getOrElse(Left(ExecutionError(s"Invalid oneOff input $value for trait ${ctx.typeName.short}"))) + case InputValue.ObjectValue(_) => Left(ExecutionError("Exactly one key must be specified for oneOff inputs")) + case _ => Left(ExecutionError(s"Can't build a trait from input $input")) + } + } + } trait ArgBuilderDerivation extends CommonArgBuilderDerivation { diff --git a/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala b/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala index bb0913148c..ba394473e9 100644 --- a/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala +++ b/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala @@ -1,47 +1,69 @@ package caliban.schema import caliban.CalibanError.ExecutionError -import caliban.{ CalibanError, InputValue } +import caliban.{ schema, CalibanError, InputValue } import caliban.Value.* import caliban.schema.macros.Macros -import caliban.schema.Annotations.GQLDefault -import caliban.schema.Annotations.GQLName +import caliban.schema.Annotations.{ GQLDefault, GQLName } +import scala.collection.mutable +import scala.collection.mutable.ListBuffer import scala.deriving.Mirror import scala.compiletime.* import scala.util.NotGiven trait CommonArgBuilderDerivation { + + // For source compat inline def recurse[P, Label, A <: Tuple]( inline values: List[(String, List[Any], ArgBuilder[Any])] = Nil + ): List[(String, List[Any], ArgBuilder[Any])] = + _recurse[P, Label, A](ListBuffer.empty ++= values) + + private inline def _recurse[P, Label, A <: Tuple]( + inline values: ListBuffer[(String, List[Any], ArgBuilder[Any])] = + ListBuffer.empty[(String, List[Any], ArgBuilder[Any])] ): List[(String, List[Any], ArgBuilder[Any])] = inline erasedValue[(Label, A)] match { - case (_: EmptyTuple, _) => values.reverse + case (_: EmptyTuple, _) => values.result() case (_: (name *: names), _: (t *: ts)) => - recurse[P, names, ts]( - ( - constValue[name].toString, - Macros.annotations[t], { - if (Macros.isEnumField[P, t]) - if (!Macros.implicitExists[ArgBuilder[t]]) derived[t] + _recurse[P, names, ts]( + values.addOne( + ( + constValue[name].toString, + Macros.annotations[t], { + inline if (Macros.isEnumField[P, t]) + inline if (!Macros.implicitExists[ArgBuilder[t]]) derived[t] + else summonInline[ArgBuilder[t]] else summonInline[ArgBuilder[t]] - else summonInline[ArgBuilder[t]] - }.asInstanceOf[ArgBuilder[Any]] - ) :: values + }.asInstanceOf[ArgBuilder[Any]] + ) + ) ) } inline def derived[A]: ArgBuilder[A] = inline summonInline[Mirror.Of[A]] match { case m: Mirror.SumOf[A] => - makeSumArgBuilder[A]( - recurse[A, m.MirroredElemLabels, m.MirroredElemTypes](), - constValue[m.MirroredLabel] - ) + inline if (Macros.hasOneOfInputAnnotation[A]) { + inline if (Macros.isValidOneOffInput[A]) + makeOneOffBuilder[A]( + _recurse[A, m.MirroredElemLabels, m.MirroredElemTypes](), + constValue[m.MirroredLabel] + ) + else + error( + "Invalid oneOff input. OneOff inputs must be sealed traits with 2 or more case classes extending them that:\n\t1. Have a single non-nullable field\n\t2. Do not have duplicated field names\n\t" + ) + } else + makeSumArgBuilder[A]( + _recurse[A, m.MirroredElemLabels, m.MirroredElemTypes](), + constValue[m.MirroredLabel] + ) case m: Mirror.ProductOf[A] => makeProductArgBuilder( - recurse[A, m.MirroredElemLabels, m.MirroredElemTypes](), + _recurse[A, m.MirroredElemLabels, m.MirroredElemTypes](), Macros.paramAnnotations[A].to(Map) )(m.fromProduct) } @@ -49,7 +71,7 @@ trait CommonArgBuilderDerivation { private def makeSumArgBuilder[A]( _subTypes: => List[(String, List[Any], ArgBuilder[Any])], _traitLabel: => String - ) = new ArgBuilder[A] { + ): ArgBuilder[A] = new ArgBuilder[A] { private lazy val subTypes = _subTypes private lazy val traitLabel = _traitLabel private val emptyInput = InputValue.ObjectValue(Map()) @@ -73,10 +95,28 @@ trait CommonArgBuilderDerivation { } } + private def makeOneOffBuilder[A]( + _subTypes: => List[(String, List[Any], ArgBuilder[Any])], + _traitLabel: => String + ): ArgBuilder[A] = new ArgBuilder.OneOff[A] { + private lazy val builders = _subTypes.map(_._3).asInstanceOf[List[ArgBuilder[A]]] + private lazy val traitLabel = _traitLabel + + def build(input: InputValue): Either[ExecutionError, A] = input match { + case InputValue.ObjectValue(value) if value.sizeCompare(1) == 0 => + builders.view + .map(_.build(input)) + .find(_.isRight) + .getOrElse(Left(ExecutionError(s"Invalid oneOff input $value for trait $traitLabel"))) + case InputValue.ObjectValue(_) => Left(ExecutionError("Exactly one key must be specified for oneOff inputs")) + case _ => Left(ExecutionError(s"Can't build a trait from input $input")) + } + } + private def makeProductArgBuilder[A]( _fields: => List[(String, List[Any], ArgBuilder[Any])], _annotations: => Map[String, List[Any]] - )(fromProduct: Product => A) = new ArgBuilder[A] { + )(fromProduct: Product => A): ArgBuilder[A] = new ArgBuilder[A] { private lazy val fields = _fields private lazy val annotations = _annotations diff --git a/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala index 799986c864..77896ef1aa 100644 --- a/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala @@ -8,6 +8,7 @@ import caliban.schema.Step.ObjectStep import caliban.schema.Types.* import caliban.schema.macros.{ Macros, TypeInfo } +import scala.collection.mutable.ListBuffer import scala.compiletime.* import scala.deriving.Mirror import scala.util.NotGiven @@ -36,25 +37,34 @@ trait CommonSchemaDerivation { case _ => s"${name}Input" } + // For source compat inline def recurse[R, P, Label, A <: Tuple]( - inline values: List[(String, List[Any], Schema[R, Any], Int)] = Nil + inline values: List[(String, List[Any], Schema[R, Any], Int)] + )(inline index: Int = 0): List[(String, List[Any], Schema[R, Any], Int)] = + _recurse(ListBuffer.empty ++= values)(index) + + private inline def _recurse[R, P, Label, A <: Tuple]( + inline values: ListBuffer[(String, List[Any], Schema[R, Any], Int)] = + ListBuffer.empty[(String, List[Any], Schema[R, Any], Int)] )(inline index: Int = 0): List[(String, List[Any], Schema[R, Any], Int)] = inline erasedValue[(Label, A)] match { - case (_: EmptyTuple, _) => values.reverse + case (_: EmptyTuple, _) => values.result() case (_: (name *: names), _: (t *: ts)) => - recurse[R, P, names, ts] { + _recurse[R, P, names, ts] { inline if (Macros.isFieldExcluded[P, name]) values else - ( - constValue[name].toString, - Macros.annotations[t], { - if (Macros.isEnumField[P, t]) - if (!Macros.implicitExists[Schema[R, t]]) derived[R, t] + values.addOne( + ( + constValue[name].toString, + Macros.annotations[t], { + inline if (Macros.isEnumField[P, t]) + inline if (!Macros.implicitExists[Schema[R, t]]) derived[R, t] + else summonInline[Schema[R, t]] else summonInline[Schema[R, t]] - else summonInline[Schema[R, t]] - }.asInstanceOf[Schema[R, Any]], - index - ) :: values + }.asInstanceOf[Schema[R, Any]], + index + ) + ) }(index + 1) } @@ -62,14 +72,14 @@ trait CommonSchemaDerivation { inline summonInline[Mirror.Of[A]] match { case m: Mirror.SumOf[A] => makeSumSchema[R, A]( - recurse[R, A, m.MirroredElemLabels, m.MirroredElemTypes]()(), + _recurse[R, A, m.MirroredElemLabels, m.MirroredElemTypes]()(), Macros.typeInfo[A], Macros.annotations[A] )(m.ordinal) case m: Mirror.ProductOf[A] => makeProductSchema[R, A]( - recurse[R, A, m.MirroredElemLabels, m.MirroredElemTypes]()(), + _recurse[R, A, m.MirroredElemLabels, m.MirroredElemTypes]()(), Macros.typeInfo[A], Macros.annotations[A], Macros.paramAnnotations[A].toMap diff --git a/core/src/main/scala-3/caliban/schema/macros/Macros.scala b/core/src/main/scala-3/caliban/schema/macros/Macros.scala index 4a9f129b15..5bb365106e 100644 --- a/core/src/main/scala-3/caliban/schema/macros/Macros.scala +++ b/core/src/main/scala-3/caliban/schema/macros/Macros.scala @@ -1,9 +1,8 @@ package caliban.schema.macros -import caliban.schema.Annotations.GQLExcluded +import caliban.schema.Annotations.{ GQLExcluded, GQLOneOfInput } import scala.quoted.* -import scala.compiletime.* private[caliban] object Macros { // this code was inspired from WIP in magnolia @@ -15,6 +14,8 @@ private[caliban] object Macros { inline def isFieldExcluded[P, T]: Boolean = ${ isFieldExcludedImpl[P, T] } inline def isEnumField[P, T]: Boolean = ${ isEnumFieldImpl[P, T] } inline def implicitExists[T]: Boolean = ${ implicitExistsImpl[T] } + inline def hasOneOfInputAnnotation[P]: Boolean = ${ hasOneOfInputAnnotationImpl[P] } + inline def isValidOneOffInput[P]: Boolean = ${ isValidOneOffInputImpl[P] } def annotationsImpl[T: Type](using qctx: Quotes): Expr[List[Any]] = { import qctx.reflect.* @@ -88,4 +89,26 @@ private[caliban] object Macros { Expr(TypeRepr.of[P].typeSymbol.flags.is(Flags.Enum) && TypeRepr.of[T].typeSymbol.flags.is(Flags.Enum)) } + def hasOneOfInputAnnotationImpl[T: Type](using q: Quotes): Expr[Boolean] = { + import q.reflect.* + Expr(TypeRepr.of[T].typeSymbol.annotations.exists(_.tpe.typeSymbol.name == "GQLOneOfInput")) + } + + def isValidOneOffInputImpl[T: Type](using q: Quotes): Expr[Boolean] = { + import q.reflect.* + val tpe = TypeRepr.of[T].typeSymbol + val flags = tpe.flags + if (flags.is(Flags.Sealed) && flags.is(Flags.Trait)) { + val constructors = tpe.children.map(_.primaryConstructor) + val children = constructors.map(_.paramSymss.flatten.map(_.name)) + val size = children.size + Expr( + size >= 2 + && children.forall(_.size == 1) + && size == children.flatten.distinct.size + && !constructors.exists(_.signature.paramSigs.contains("scala.Option")) + ) + } else Expr(false) + } + } diff --git a/core/src/main/scala/caliban/schema/Annotations.scala b/core/src/main/scala/caliban/schema/Annotations.scala index 9689a206ac..f98093e7f7 100644 --- a/core/src/main/scala/caliban/schema/Annotations.scala +++ b/core/src/main/scala/caliban/schema/Annotations.scala @@ -63,4 +63,9 @@ object Annotations { * Annotation to specify the default value of an input field */ case class GQLDefault(value: String) extends StaticAnnotation + + /** + * Annotation to make a sealed trait as a GraphQL @oneOff input + */ + case class GQLOneOfInput() extends StaticAnnotation } diff --git a/core/src/main/scala/caliban/schema/ArgBuilder.scala b/core/src/main/scala/caliban/schema/ArgBuilder.scala index 866a8317aa..a1dd2f46d9 100644 --- a/core/src/main/scala/caliban/schema/ArgBuilder.scala +++ b/core/src/main/scala/caliban/schema/ArgBuilder.scala @@ -80,6 +80,8 @@ trait ArgBuilder[T] { self => object ArgBuilder extends ArgBuilderInstances { object auto extends AutoArgBuilderDerivation + + private[caliban] trait OneOff[T] extends ArgBuilder[T] } trait ArgBuilderInstances extends ArgBuilderDerivation { diff --git a/core/src/test/scala/caliban/schema/ArgBuilderSpec.scala b/core/src/test/scala/caliban/schema/ArgBuilderSpec.scala index 6c92e9a0e9..cea9fc2d8b 100644 --- a/core/src/test/scala/caliban/schema/ArgBuilderSpec.scala +++ b/core/src/test/scala/caliban/schema/ArgBuilderSpec.scala @@ -5,6 +5,7 @@ import caliban.InputValue import caliban.InputValue.ObjectValue import caliban.schema.ArgBuilder.auto._ import caliban.Value.{ IntValue, NullValue, StringValue } +import caliban.schema.Annotations.GQLOneOfInput import zio.test.Assertion._ import zio.test._ @@ -96,6 +97,42 @@ object ArgBuilderSpec extends ZIOSpecDefault { assertTrue(derivedAB.build(ObjectValue(Map("a" -> NullValue))) == Right(Wrapper(NullNullable))) && assertTrue(derivedAB.build(ObjectValue(Map("a" -> StringValue("x")))) == Right(Wrapper(SomeNullable("x")))) } - ) + ), + suite("oneOff") { + @GQLOneOfInput + sealed trait Foo + + object Foo { + case class FooString(stringValue: String) extends Foo + + case class FooInt(intValue: Int) extends Foo + } + + implicit val fooStringAb: ArgBuilder[Foo.FooString] = ArgBuilder.gen + implicit val fooIntAb: ArgBuilder[Foo.FooInt] = ArgBuilder.gen + val fooAb: ArgBuilder[Foo] = ArgBuilder.gen + + List( + test("valid input") { + assertTrue( + fooAb.build( + ObjectValue(Map("stringValue" -> StringValue("foo"))) + ) == Right(Foo.FooString("foo")), + fooAb.build(ObjectValue(Map("intValue" -> IntValue(42)))) == Right(Foo.FooInt(42)) + ) + }, + test("invalid input") { + List( + Map("invalid" -> StringValue("foo")), + Map("stringValue" -> StringValue("foo"), "intValue" -> IntValue(42)), + Map("stringValue" -> NullValue), + Map("stringValue" -> NullValue, "invalid" -> NullValue) + ) + .map(input => assertTrue(fooAb.build(ObjectValue(input)).isLeft)) + .foldLeft(assertCompletes)(_ && _) + + } + ) + } ) } From 1b37f6bc31f0750e1ae564087e78bbc0e55dfbd5 Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Mon, 21 Aug 2023 00:56:46 +0530 Subject: [PATCH 02/25] Add rendering and introspection of `oneOf` inputs --- .../caliban/client/IntrospectionClient.scala | 2 + .../caliban/schema/ArgBuilderDerivation.scala | 8 ++-- .../caliban/schema/SchemaDerivation.scala | 42 ++++++++++++++++--- .../caliban/schema/ArgBuilderDerivation.scala | 8 ++-- .../caliban/introspection/Introspector.scala | 16 ++++++- .../introspection/adt/__InputValue.scala | 2 + .../caliban/introspection/adt/__Type.scala | 15 +++++-- .../caliban/parsing/adt/Definition.scala | 3 +- .../caliban/parsing/parsers/Parsers.scala | 3 +- .../scala/caliban/schema/Annotations.scala | 2 +- .../scala/caliban/schema/ArgBuilder.scala | 2 - .../main/scala/caliban/schema/Schema.scala | 10 +++-- .../src/main/scala/caliban/schema/Types.scala | 6 ++- .../validation/FragmentValidator.scala | 2 +- .../test/scala/caliban/RenderingSpec.scala | 40 +++++++++++++++++- core/src/test/scala/caliban/TestUtils.scala | 23 ++++++++++ .../introspection/IntrospectionSpec.scala | 15 ++++++- .../scala/caliban/parsing/ParserSpec.scala | 6 ++- .../scala/caliban/schema/ArgBuilderSpec.scala | 7 ++-- .../scala/caliban/tools/ClientWriter.scala | 24 +++++------ .../caliban/tools/IntrospectionClient.scala | 16 +++++-- 21 files changed, 197 insertions(+), 55 deletions(-) diff --git a/client/src/main/scala/caliban/client/IntrospectionClient.scala b/client/src/main/scala/caliban/client/IntrospectionClient.scala index abf7a76d42..9baa915594 100644 --- a/client/src/main/scala/caliban/client/IntrospectionClient.scala +++ b/client/src/main/scala/caliban/client/IntrospectionClient.scala @@ -153,6 +153,8 @@ object IntrospectionClient { Field("inputFields", OptionOf(ListOf(Obj(innerSelection)))) def ofType[A](innerSelection: SelectionBuilder[__Type, A]): SelectionBuilder[__Type, Option[A]] = Field("ofType", OptionOf(Obj(innerSelection))) + def isOneOf: SelectionBuilder[__Type, Option[Boolean]] = + Field("isOneOf", OptionOf(Scalar[Boolean]())) } type __Field diff --git a/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala b/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala index a6c1462023..cd0cd6d412 100644 --- a/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala @@ -38,7 +38,7 @@ trait CommonArgBuilderDerivation { } def dispatch[T](ctx: SealedTrait[ArgBuilder, T]): ArgBuilder[T] = - if (ctx.annotations.contains(GQLOneOfInput())) makeOneOffBuilder(ctx) + if (ctx.annotations.collectFirst { case GQLOneOfInput(_) => () }.isDefined) makeOneOffBuilder(ctx) else makeSumBuilder(ctx) private def makeSumBuilder[T](ctx: SealedTrait[ArgBuilder, T]): ArgBuilder[T] = input => @@ -58,7 +58,7 @@ trait CommonArgBuilderDerivation { case None => Left(ExecutionError(s"Can't build a trait from input $input")) } - private def makeOneOffBuilder[A](ctx: SealedTrait[ArgBuilder, A]): ArgBuilder[A] = new ArgBuilder.OneOff[A] { + private def makeOneOffBuilder[A](ctx: SealedTrait[ArgBuilder, A]): ArgBuilder[A] = new ArgBuilder[A] { private lazy val builders = ctx.subtypes.map(_.typeclass) def build(input: InputValue): Either[ExecutionError, A] = input match { @@ -66,8 +66,8 @@ trait CommonArgBuilderDerivation { builders.view .map(_.build(input)) .find(_.isRight) - .getOrElse(Left(ExecutionError(s"Invalid oneOff input $value for trait ${ctx.typeName.short}"))) - case InputValue.ObjectValue(_) => Left(ExecutionError("Exactly one key must be specified for oneOff inputs")) + .getOrElse(Left(ExecutionError(s"Invalid oneOf input $value for trait ${ctx.typeName.short}"))) + case InputValue.ObjectValue(_) => Left(ExecutionError("Exactly one key must be specified for oneOf inputs")) case _ => Left(ExecutionError(s"Can't build a trait from input $input")) } } diff --git a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala index fa5d277eca..3b4529009f 100644 --- a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala @@ -40,7 +40,7 @@ trait CommonSchemaDerivation[R] { if ((ctx.isValueClass || isValueType(ctx)) && ctx.parameters.nonEmpty) { if (isScalarValueType(ctx)) makeScalar(getName(ctx), getDescription(ctx)) else ctx.parameters.head.typeclass.toType_(isInput, isSubscription) - } else if (isInput) + } else if (isInput) { makeInputObject( Some(ctx.annotations.collectFirst { case GQLInputName(suffix) => suffix } .getOrElse(customizeInputTypeName(getName(ctx)))), @@ -61,7 +61,7 @@ trait CommonSchemaDerivation[R] { Some(ctx.typeName.full), Some(getDirectives(ctx)) ) - else + } else makeObject( Some(getName(ctx)), getDescription(ctx), @@ -98,6 +98,8 @@ trait CommonSchemaDerivation[R] { } def dispatch[T](ctx: SealedTrait[Typeclass, T]): Typeclass[T] = new Typeclass[T] { + private lazy val isOneOffInputName = ctx.annotations.collectFirst { case GQLOneOfInput(name) => name } + override def toType(isInput: Boolean, isSubscription: Boolean): __Type = { val subtypes = ctx.subtypes @@ -122,11 +124,11 @@ trait CommonSchemaDerivation[R] { case _ => false } - if (isEnum && subtypes.nonEmpty && !isInterface && !isUnion) + if (isEnum && subtypes.nonEmpty && !isInterface && !isUnion && isOneOffInputName.isEmpty) { makeEnum( Some(getName(ctx)), getDescription(ctx), - subtypes.collect { case (__Type(_, Some(name), description, _, _, _, _, _, _, _, _, _), annotations) => + subtypes.collect { case (__Type(_, Some(name), description, _, _, _, _, _, _, _, _, _, _), annotations) => __EnumValue( name, description, @@ -138,7 +140,35 @@ trait CommonSchemaDerivation[R] { Some(ctx.typeName.full), Some(getDirectives(ctx.annotations)) ) - else if (!isInterface) + } else if (isOneOffInputName.isDefined) { + makeInputObject( + None, + None, + List( + __InputValue( + isOneOffInputName.getOrElse(""), + None, + () => + makeInputObject( + Some(ctx.annotations.collectFirst { case GQLInputName(suffix) => suffix } + .getOrElse(customizeInputTypeName(getName(ctx)))), + getDescription(ctx), + ctx.subtypes + .flatMap(_.typeclass.toType_(isInput = true).inputFields.getOrElse(Nil)) + .toList + .map(_.nullable), + Some(ctx.typeName.full), + Some(List(Directive("oneOf"))), + isOneOf = true + ).nonNull, + None + ) + ), + Some(ctx.typeName.full), + Some(List(Directive("oneOf"))), + isOneOf = true + ) + } else if (!isInterface) { makeUnion( Some(getName(ctx)), getDescription(ctx), @@ -146,7 +176,7 @@ trait CommonSchemaDerivation[R] { Some(ctx.typeName.full), Some(getDirectives(ctx.annotations)) ) - else { + } else { val impl = subtypes.map(_._1.copy(interfaces = () => Some(List(toType(isInput, isSubscription))))) val commonFields = () => impl diff --git a/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala b/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala index ba394473e9..e81bddbbeb 100644 --- a/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala +++ b/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala @@ -53,7 +53,7 @@ trait CommonArgBuilderDerivation { ) else error( - "Invalid oneOff input. OneOff inputs must be sealed traits with 2 or more case classes extending them that:\n\t1. Have a single non-nullable field\n\t2. Do not have duplicated field names\n\t" + "Invalid oneOf input. OneOff inputs must be sealed traits with 2 or more case classes extending them that:\n\t1. Have a single non-nullable field\n\t2. Do not have duplicated field names\n\t" ) } else makeSumArgBuilder[A]( @@ -98,7 +98,7 @@ trait CommonArgBuilderDerivation { private def makeOneOffBuilder[A]( _subTypes: => List[(String, List[Any], ArgBuilder[Any])], _traitLabel: => String - ): ArgBuilder[A] = new ArgBuilder.OneOff[A] { + ): ArgBuilder[A] = new ArgBuilder[A] { private lazy val builders = _subTypes.map(_._3).asInstanceOf[List[ArgBuilder[A]]] private lazy val traitLabel = _traitLabel @@ -107,8 +107,8 @@ trait CommonArgBuilderDerivation { builders.view .map(_.build(input)) .find(_.isRight) - .getOrElse(Left(ExecutionError(s"Invalid oneOff input $value for trait $traitLabel"))) - case InputValue.ObjectValue(_) => Left(ExecutionError("Exactly one key must be specified for oneOff inputs")) + .getOrElse(Left(ExecutionError(s"Invalid oneOf input $value for trait $traitLabel"))) + case InputValue.ObjectValue(_) => Left(ExecutionError("Exactly one key must be specified for oneOf inputs")) case _ => Left(ExecutionError(s"Can't build a trait from input $input")) } } diff --git a/core/src/main/scala/caliban/introspection/Introspector.scala b/core/src/main/scala/caliban/introspection/Introspector.scala index 11565d14b0..064b4a1317 100644 --- a/core/src/main/scala/caliban/introspection/Introspector.scala +++ b/core/src/main/scala/caliban/introspection/Introspector.scala @@ -44,6 +44,17 @@ object Introspector extends IntrospectionDerivation { ) ) + private val oneOfDirective = + __Directive( + "oneOf", + Some( + "The `@oneOf` directive is used within the type system definition language to indicate an Input Object is a OneOf Input Object." + ), + Set(__DirectiveLocation.INPUT_OBJECT), + Nil, + isRepeatable = false + ) + /** * Generates a schema for introspecting the given type. */ @@ -67,6 +78,9 @@ object Introspector extends IntrospectionDerivation { .values .toList .sortBy(_.name.getOrElse("")) + + val hasOneOf = types.exists(_.isOneOf.getOrElse(false)) + val resolver = __Introspection( __Schema( rootType.description, @@ -74,7 +88,7 @@ object Introspector extends IntrospectionDerivation { rootType.mutationType, rootType.subscriptionType, types, - directives ++ rootType.additionalDirectives + directives ++ (if (hasOneOf) List(oneOfDirective) else Nil) ++ rootType.additionalDirectives ), args => types.find(_.name.contains(args.name)) ) diff --git a/core/src/main/scala/caliban/introspection/adt/__InputValue.scala b/core/src/main/scala/caliban/introspection/adt/__InputValue.scala index 44fe72fcbf..b699b411fe 100644 --- a/core/src/main/scala/caliban/introspection/adt/__InputValue.scala +++ b/core/src/main/scala/caliban/introspection/adt/__InputValue.scala @@ -17,4 +17,6 @@ case class __InputValue( } private[caliban] lazy val _type: __Type = `type`() + + private[caliban] def nullable: __InputValue = copy(`type` = () => _type.nullable) } diff --git a/core/src/main/scala/caliban/introspection/adt/__Type.scala b/core/src/main/scala/caliban/introspection/adt/__Type.scala index 0f930d235c..ec8354fb39 100644 --- a/core/src/main/scala/caliban/introspection/adt/__Type.scala +++ b/core/src/main/scala/caliban/introspection/adt/__Type.scala @@ -19,7 +19,8 @@ case class __Type( ofType: Option[__Type] = None, specifiedBy: Option[String] = None, directives: Option[List[Directive]] = None, - origin: Option[String] = None + origin: Option[String] = None, + isOneOf: Option[Boolean] = None ) { self => def |+|(that: __Type): __Type = __Type( kind, @@ -105,7 +106,8 @@ case class __Type( description, name.getOrElse(""), directives.getOrElse(Nil), - inputFields.getOrElse(Nil).map(_.toInputValueDefinition) + inputFields.getOrElse(Nil).map(_.toInputValueDefinition), + isOneOf.getOrElse(false) ) ) case _ => None @@ -117,8 +119,13 @@ case class __Type( case _ => true } - def list: __Type = __Type(__TypeKind.LIST, ofType = Some(self)) - def nonNull: __Type = __Type(__TypeKind.NON_NULL, ofType = Some(self)) + def list: __Type = __Type(__TypeKind.LIST, ofType = Some(self)) + def nonNull: __Type = __Type(__TypeKind.NON_NULL, ofType = Some(self)) + def nullable: __Type = + (kind, ofType) match { + case (__TypeKind.NON_NULL, Some(inner)) => inner + case _ => self + } lazy val allFields: List[__Field] = fields(__DeprecatedArgs(Some(true))).getOrElse(Nil) diff --git a/core/src/main/scala/caliban/parsing/adt/Definition.scala b/core/src/main/scala/caliban/parsing/adt/Definition.scala index 0bace1caa3..5d15865c64 100644 --- a/core/src/main/scala/caliban/parsing/adt/Definition.scala +++ b/core/src/main/scala/caliban/parsing/adt/Definition.scala @@ -108,7 +108,8 @@ object Definition { description: Option[String], name: String, directives: List[Directive], - fields: List[InputValueDefinition] + fields: List[InputValueDefinition], + isOneOf: Boolean ) extends TypeDefinition { override def toString: String = "Input Object" } diff --git a/core/src/main/scala/caliban/parsing/parsers/Parsers.scala b/core/src/main/scala/caliban/parsing/parsers/Parsers.scala index 5e6b158f37..1dc5eb2ac5 100644 --- a/core/src/main/scala/caliban/parsing/parsers/Parsers.scala +++ b/core/src/main/scala/caliban/parsing/parsers/Parsers.scala @@ -92,7 +92,8 @@ object Parsers extends SelectionParsers { description.map(_.value), name, directives = directives.getOrElse(Nil), - fields = fields.fold(List[InputValueDefinition]())(_.toList) + fields = fields.fold(List[InputValueDefinition]())(_.toList), + isOneOf = directives.fold(false)(_.exists(_.name == "oneOf")) ) } diff --git a/core/src/main/scala/caliban/schema/Annotations.scala b/core/src/main/scala/caliban/schema/Annotations.scala index f98093e7f7..240a63afc8 100644 --- a/core/src/main/scala/caliban/schema/Annotations.scala +++ b/core/src/main/scala/caliban/schema/Annotations.scala @@ -67,5 +67,5 @@ object Annotations { /** * Annotation to make a sealed trait as a GraphQL @oneOff input */ - case class GQLOneOfInput() extends StaticAnnotation + case class GQLOneOfInput(fieldName: String) extends StaticAnnotation } diff --git a/core/src/main/scala/caliban/schema/ArgBuilder.scala b/core/src/main/scala/caliban/schema/ArgBuilder.scala index a1dd2f46d9..866a8317aa 100644 --- a/core/src/main/scala/caliban/schema/ArgBuilder.scala +++ b/core/src/main/scala/caliban/schema/ArgBuilder.scala @@ -80,8 +80,6 @@ trait ArgBuilder[T] { self => object ArgBuilder extends ArgBuilderInstances { object auto extends AutoArgBuilderDerivation - - private[caliban] trait OneOff[T] extends ArgBuilder[T] } trait ArgBuilderInstances extends ArgBuilderDerivation { diff --git a/core/src/main/scala/caliban/schema/Schema.scala b/core/src/main/scala/caliban/schema/Schema.scala index ffda14506f..90565b40d0 100644 --- a/core/src/main/scala/caliban/schema/Schema.scala +++ b/core/src/main/scala/caliban/schema/Schema.scala @@ -168,7 +168,8 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { fields(isInput, isSubscription).map { case (f, _) => __InputValue(f.name, f.description, f.`type`, None, f.directives) }, - directives = Some(directives) + directives = Some(directives), + isOneOf = directives.exists(_.name == "oneOf") ) } else makeObject(Some(name), description, fields(isInput, isSubscription).map(_._1), directives) @@ -387,9 +388,10 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { ev2: Schema[RB, B] ): Schema[RB, A => B] = new Schema[RB, A => B] { - private lazy val inputType = ev1.toType_(true) - private val unwrappedArgumentName = "value" - override def arguments: List[__InputValue] = + private lazy val inputType = ev1.toType_(true) + private val unwrappedArgumentName = "value" + + override lazy val arguments: List[__InputValue] = inputType.inputFields.getOrElse( handleInput(List.empty[__InputValue])( List( diff --git a/core/src/main/scala/caliban/schema/Types.scala b/core/src/main/scala/caliban/schema/Types.scala index c6c8d4aa2c..dbb8620809 100644 --- a/core/src/main/scala/caliban/schema/Types.scala +++ b/core/src/main/scala/caliban/schema/Types.scala @@ -66,7 +66,8 @@ object Types { description: Option[String], fields: List[__InputValue], origin: Option[String] = None, - directives: Option[List[Directive]] = None + directives: Option[List[Directive]] = None, + isOneOf: Boolean = false ): __Type = __Type( __TypeKind.INPUT_OBJECT, @@ -74,7 +75,8 @@ object Types { description, inputFields = Some(fields), origin = origin, - directives = directives + directives = directives, + isOneOf = Some(isOneOf) ) def makeUnion( diff --git a/core/src/main/scala/caliban/validation/FragmentValidator.scala b/core/src/main/scala/caliban/validation/FragmentValidator.scala index 929cab6c79..5100b40498 100644 --- a/core/src/main/scala/caliban/validation/FragmentValidator.scala +++ b/core/src/main/scala/caliban/validation/FragmentValidator.scala @@ -105,7 +105,7 @@ object FragmentValidator { fields.foreach { case field @ SelectedField( - __Type(_, Some(name), _, _, _, _, _, _, _, _, _, _), + __Type(_, Some(name), _, _, _, _, _, _, _, _, _, _, _), _, _ ) if isConcrete(field.parentType) => diff --git a/core/src/test/scala/caliban/RenderingSpec.scala b/core/src/test/scala/caliban/RenderingSpec.scala index a9b8407921..49880959f4 100644 --- a/core/src/test/scala/caliban/RenderingSpec.scala +++ b/core/src/test/scala/caliban/RenderingSpec.scala @@ -2,7 +2,7 @@ package caliban import caliban.CalibanError.ParsingError import caliban.TestUtils._ -import caliban.introspection.adt.{ __Type, __TypeKind } +import caliban.introspection.adt.{ __Directive, __DirectiveLocation, __Type, __TypeKind } import caliban.parsing.Parser import caliban.parsing.adt.Definition.{ TypeSystemDefinition, TypeSystemExtension } import caliban.parsing.adt.Definition.TypeSystemDefinition.TypeDefinition @@ -12,6 +12,8 @@ import caliban.parsing.adt.Definition.TypeSystemDefinition.TypeDefinition.{ InputValueDefinition } import caliban.parsing.adt.{ Definition, Directive } +import caliban.schema.Annotations.GQLOneOfInput +import caliban.schema.{ ArgBuilder, Schema } import caliban.schema.Schema.auto._ import caliban.schema.ArgBuilder.auto._ import zio.IO @@ -170,6 +172,42 @@ object RenderingSpec extends ZIOSpecDefault { test("it should render multi line descriptions ending in quote") { val api = graphQL(resolver) checkApi(api) + }, + test("foo") { + + @GQLOneOfInput("fooInput") + sealed trait Foo + + object Foo { + case class FooString(stringValue: String) extends Foo + case class FooInt(intValue: Int) extends Foo + } + + case class Queries(foo: Foo => String) + + implicit val fooIntAb: ArgBuilder[Foo.FooInt] = ArgBuilder.gen + implicit val fooStringAb: ArgBuilder[Foo.FooString] = ArgBuilder.gen + implicit val fooAb: ArgBuilder[Foo] = ArgBuilder.gen + implicit val fooSchema: Schema[Any, Foo] = Schema.gen + implicit val schema: Schema[Any, Queries] = Schema.gen + + val resolver = RootResolver(Queries(_.toString)) + + val expected = + """schema { + | query: Queries + |} + | + |input FooInput @oneOf { + | intValue: Int + | stringValue: String + |} + | + |type Queries { + | foo(fooInput: FooInput!): String! + |}""".stripMargin + + assertTrue(graphQL(resolver).render == expected) } ) } diff --git a/core/src/test/scala/caliban/TestUtils.scala b/core/src/test/scala/caliban/TestUtils.scala index dde833dc87..20e60fec02 100644 --- a/core/src/test/scala/caliban/TestUtils.scala +++ b/core/src/test/scala/caliban/TestUtils.scala @@ -103,6 +103,13 @@ object TestUtils { case class CharacterInArgs(@GQLDirective(Directive("lowercase")) names: List[String]) case class CharacterObjectArgs(character: CharacterInput) + @GQLOneOfInput("nameOrOrigin") + sealed trait NameOrOrigin + object NameOrOrigin { + case class ByName(name: String) extends NameOrOrigin + case class ByOrigin(origin: Origin) extends NameOrOrigin + } + @GQLDescription("Queries") case class Query( @GQLDescription("Return all characters from a given origin") characters: CharactersArgs => List[Character], @@ -118,6 +125,12 @@ object TestUtils { @GQLDeprecated("Use `characters`") character: CharacterArgs => UIO[Option[Character]] ) + @GQLDescription("Queries") + case class QueryWithOneOf( + exists: CharacterObjectArgs => Boolean, + character: NameOrOrigin => List[Character] + ) + @GQLDescription("Mutations") case class MutationIO(deleteCharacter: CharacterArgs => UIO[Unit]) @@ -126,6 +139,7 @@ object TestUtils { implicit val characterSchema: Schema[Any, Character] = genAll implicit val querySchema: Schema[Any, Query] = genAll implicit val queryIOSchema: Schema[Any, QueryIO] = genAll + implicit val queryOneOffSchema: Schema[Any, QueryWithOneOf] = genAll implicit val mutationIOSchema: Schema[Any, MutationIO] = genAll implicit val subscriptionIOSchema: Schema[Any, SubscriptionIO] = genAll @@ -144,6 +158,15 @@ object TestUtils { args => ZIO.succeed(characters.find(c => c.name == args.name)) ) ) + val resolverOneOf = RootResolver( + QueryWithOneOf( + args => characters.exists(_.name == args.character.name), + { + case NameOrOrigin.ByName(name) => characters.filter(c => c.name == name) + case NameOrOrigin.ByOrigin(origin) => characters.filter(c => c.origin == origin) + } + ) + ) val resolverWithMutation = RootResolver( resolverIO.queryResolver, MutationIO(_ => ZIO.unit) diff --git a/core/src/test/scala/caliban/introspection/IntrospectionSpec.scala b/core/src/test/scala/caliban/introspection/IntrospectionSpec.scala index 6c321a5389..9009652b85 100644 --- a/core/src/test/scala/caliban/introspection/IntrospectionSpec.scala +++ b/core/src/test/scala/caliban/introspection/IntrospectionSpec.scala @@ -36,6 +36,7 @@ object IntrospectionSpec extends ZIOSpecDefault { kind name description + isOneOf fields(includeDeprecated: true) { name description @@ -125,7 +126,7 @@ object IntrospectionSpec extends ZIOSpecDefault { interpreter.flatMap(_.execute(fullIntrospectionQuery)).map { response => assertTrue( response.data.toString == - """{"__schema":{"queryType":{"name":"QueryIO"},"mutationType":null,"subscriptionType":null,"types":[{"kind":"SCALAR","name":"Boolean","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Captain","description":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"CaptainShipName","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"CaptainShipName","description":"Description of custom scalar emphasizing proper captain ship names","fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Character","description":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"nicknames","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"origin","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"Origin","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"role","description":null,"args":[],"type":{"kind":"UNION","name":"Role","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Engineer","description":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Float","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Int","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Mechanic","description":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"Origin","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"BELT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"EARTH","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MARS","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MOON","description":null,"isDeprecated":true,"deprecationReason":"Use: EARTH | MARS | BELT"}],"possibleTypes":null},{"kind":"OBJECT","name":"Pilot","description":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"QueryIO","description":"Queries","fields":[{"name":"characters","description":"Return all characters from a given origin","args":[{"name":"origin","description":null,"type":{"kind":"ENUM","name":"Origin","ofType":null},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"Character","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"character","description":null,"args":[{"name":"name","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}],"type":{"kind":"OBJECT","name":"Character","ofType":null},"isDeprecated":true,"deprecationReason":"Use `characters`"}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"UNION","name":"Role","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":[{"kind":"OBJECT","name":"Captain","ofType":null},{"kind":"OBJECT","name":"Engineer","ofType":null},{"kind":"OBJECT","name":"Mechanic","ofType":null},{"kind":"OBJECT","name":"Pilot","ofType":null}]},{"kind":"SCALAR","name":"String","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null}],"directives":[{"name":"skip","description":"The @skip directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional exclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"include","description":"The @include directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional inclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"specifiedBy","description":"The @specifiedBy directive is used within the type system definition language to provide a URL for specifying the behavior of custom scalar types. The URL should point to a human-readable specification of the data format, serialization, and coercion rules. It must not appear on built-in scalar types.","locations":["SCALAR"],"args":[{"name":"url","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}]}]}}""" + """{"__schema":{"queryType":{"name":"QueryIO"},"mutationType":null,"subscriptionType":null,"types":[{"kind":"SCALAR","name":"Boolean","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Captain","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"CaptainShipName","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"CaptainShipName","description":"Description of custom scalar emphasizing proper captain ship names","isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Character","description":null,"isOneOf":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"nicknames","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"origin","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"Origin","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"role","description":null,"args":[],"type":{"kind":"UNION","name":"Role","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Engineer","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Float","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Int","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Mechanic","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"Origin","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"BELT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"EARTH","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MARS","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MOON","description":null,"isDeprecated":true,"deprecationReason":"Use: EARTH | MARS | BELT"}],"possibleTypes":null},{"kind":"OBJECT","name":"Pilot","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"QueryIO","description":"Queries","isOneOf":null,"fields":[{"name":"characters","description":"Return all characters from a given origin","args":[{"name":"origin","description":null,"type":{"kind":"ENUM","name":"Origin","ofType":null},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"Character","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"character","description":null,"args":[{"name":"name","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}],"type":{"kind":"OBJECT","name":"Character","ofType":null},"isDeprecated":true,"deprecationReason":"Use `characters`"}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"UNION","name":"Role","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":[{"kind":"OBJECT","name":"Captain","ofType":null},{"kind":"OBJECT","name":"Engineer","ofType":null},{"kind":"OBJECT","name":"Mechanic","ofType":null},{"kind":"OBJECT","name":"Pilot","ofType":null}]},{"kind":"SCALAR","name":"String","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null}],"directives":[{"name":"skip","description":"The @skip directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional exclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"include","description":"The @include directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional inclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"specifiedBy","description":"The @specifiedBy directive is used within the type system definition language to provide a URL for specifying the behavior of custom scalar types. The URL should point to a human-readable specification of the data format, serialization, and coercion rules. It must not appear on built-in scalar types.","locations":["SCALAR"],"args":[{"name":"url","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}]}]}}""" ) } }, @@ -153,7 +154,17 @@ object IntrospectionSpec extends ZIOSpecDefault { interpreter.flatMap(_.execute(fullIntrospectionQuery)).map { response => assertTrue( response.data.toString == - """{"__schema":{"queryType":{"name":"QueryIO"},"mutationType":null,"subscriptionType":null,"types":[{"kind":"SCALAR","name":"Boolean","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Captain","description":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"CaptainShipName","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"CaptainShipName","description":"Description of custom scalar emphasizing proper captain ship names","fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Character","description":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"nicknames","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"origin","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"Origin","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"role","description":null,"args":[],"type":{"kind":"UNION","name":"Role","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Engineer","description":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Float","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Int","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Mechanic","description":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"Origin","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"BELT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"EARTH","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MARS","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MOON","description":null,"isDeprecated":true,"deprecationReason":"Use: EARTH | MARS | BELT"}],"possibleTypes":null},{"kind":"OBJECT","name":"Pilot","description":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"QueryIO","description":"Queries","fields":[{"name":"characters","description":"Return all characters from a given origin","args":[{"name":"origin","description":null,"type":{"kind":"ENUM","name":"Origin","ofType":null},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"Character","ofType":null}}}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"UNION","name":"Role","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":[{"kind":"OBJECT","name":"Captain","ofType":null},{"kind":"OBJECT","name":"Engineer","ofType":null},{"kind":"OBJECT","name":"Mechanic","ofType":null},{"kind":"OBJECT","name":"Pilot","ofType":null}]},{"kind":"SCALAR","name":"String","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null}],"directives":[{"name":"skip","description":"The @skip directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional exclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"include","description":"The @include directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional inclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"specifiedBy","description":"The @specifiedBy directive is used within the type system definition language to provide a URL for specifying the behavior of custom scalar types. The URL should point to a human-readable specification of the data format, serialization, and coercion rules. It must not appear on built-in scalar types.","locations":["SCALAR"],"args":[{"name":"url","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}]}]}}""" + """{"__schema":{"queryType":{"name":"QueryIO"},"mutationType":null,"subscriptionType":null,"types":[{"kind":"SCALAR","name":"Boolean","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Captain","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"CaptainShipName","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"CaptainShipName","description":"Description of custom scalar emphasizing proper captain ship names","isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Character","description":null,"isOneOf":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"nicknames","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"origin","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"Origin","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"role","description":null,"args":[],"type":{"kind":"UNION","name":"Role","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Engineer","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Float","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Int","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Mechanic","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"Origin","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"BELT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"EARTH","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MARS","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MOON","description":null,"isDeprecated":true,"deprecationReason":"Use: EARTH | MARS | BELT"}],"possibleTypes":null},{"kind":"OBJECT","name":"Pilot","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"QueryIO","description":"Queries","isOneOf":null,"fields":[{"name":"characters","description":"Return all characters from a given origin","args":[{"name":"origin","description":null,"type":{"kind":"ENUM","name":"Origin","ofType":null},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"Character","ofType":null}}}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"UNION","name":"Role","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":[{"kind":"OBJECT","name":"Captain","ofType":null},{"kind":"OBJECT","name":"Engineer","ofType":null},{"kind":"OBJECT","name":"Mechanic","ofType":null},{"kind":"OBJECT","name":"Pilot","ofType":null}]},{"kind":"SCALAR","name":"String","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null}],"directives":[{"name":"skip","description":"The @skip directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional exclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"include","description":"The @include directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional inclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"specifiedBy","description":"The @specifiedBy directive is used within the type system definition language to provide a URL for specifying the behavior of custom scalar types. The URL should point to a human-readable specification of the data format, serialization, and coercion rules. It must not appear on built-in scalar types.","locations":["SCALAR"],"args":[{"name":"url","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}]}]}}""" + ) + } + }, + test("introspect schema with oneOff") { + val interpreter = graphQL(resolverOneOf).interpreter + + interpreter.flatMap(_.execute(fullIntrospectionQuery)).map { response => + assertTrue( + response.data.toString == + """{"__schema":{"queryType":{"name":"QueryWithOneOf"},"mutationType":null,"subscriptionType":null,"types":[{"kind":"SCALAR","name":"Boolean","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Captain","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"CaptainShipName","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"CaptainShipName","description":"Description of custom scalar emphasizing proper captain ship names","isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Character","description":null,"isOneOf":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"nicknames","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"origin","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"Origin","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"role","description":null,"args":[],"type":{"kind":"UNION","name":"Role","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"INPUT_OBJECT","name":"CharacterInput","description":null,"isOneOf":false,"fields":null,"inputFields":[{"name":"name","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null},{"name":"nicknames","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}}}},"defaultValue":null},{"name":"origin","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"Origin","ofType":null}},"defaultValue":null}],"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Engineer","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Float","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Int","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Mechanic","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"INPUT_OBJECT","name":"NameOrOriginInput","description":null,"isOneOf":true,"fields":null,"inputFields":[{"name":"name","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"origin","description":null,"type":{"kind":"ENUM","name":"Origin","ofType":null},"defaultValue":null}],"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"Origin","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"BELT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"EARTH","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MARS","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MOON","description":null,"isDeprecated":true,"deprecationReason":"Use: EARTH | MARS | BELT"}],"possibleTypes":null},{"kind":"OBJECT","name":"Pilot","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"QueryWithOneOf","description":"Queries","isOneOf":null,"fields":[{"name":"exists","description":null,"args":[{"name":"character","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"INPUT_OBJECT","name":"CharacterInput","ofType":null}},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"character","description":null,"args":[{"name":"nameOrOrigin","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"INPUT_OBJECT","name":"NameOrOriginInput","ofType":null}},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"Character","ofType":null}}}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"UNION","name":"Role","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":[{"kind":"OBJECT","name":"Captain","ofType":null},{"kind":"OBJECT","name":"Engineer","ofType":null},{"kind":"OBJECT","name":"Mechanic","ofType":null},{"kind":"OBJECT","name":"Pilot","ofType":null}]},{"kind":"SCALAR","name":"String","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null}],"directives":[{"name":"skip","description":"The @skip directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional exclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"include","description":"The @include directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional inclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"specifiedBy","description":"The @specifiedBy directive is used within the type system definition language to provide a URL for specifying the behavior of custom scalar types. The URL should point to a human-readable specification of the data format, serialization, and coercion rules. It must not appear on built-in scalar types.","locations":["SCALAR"],"args":[{"name":"url","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}]},{"name":"oneOf","description":"The `@oneOf` directive is used within the type system definition language to indicate an Input Object is a OneOf Input Object.","locations":["INPUT_OBJECT"],"args":[]}]}}""" ) } }, diff --git a/core/src/test/scala/caliban/parsing/ParserSpec.scala b/core/src/test/scala/caliban/parsing/ParserSpec.scala index ac91a90321..b43d7aacd6 100644 --- a/core/src/test/scala/caliban/parsing/ParserSpec.scala +++ b/core/src/test/scala/caliban/parsing/ParserSpec.scala @@ -553,7 +553,8 @@ object ParserSpec extends ZIOSpecDefault { description = None, name = "BarBaz", directives = Nil, - fields = Nil + fields = Nil, + isOneOf = false ) ), sourceMapper = SourceMapper.apply(inputWithNoBody) @@ -571,7 +572,8 @@ object ParserSpec extends ZIOSpecDefault { description = None, name = "BarBaz", directives = Nil, - fields = Nil + fields = Nil, + isOneOf = false ) ), sourceMapper = SourceMapper.apply(inputWithEmptyBody) diff --git a/core/src/test/scala/caliban/schema/ArgBuilderSpec.scala b/core/src/test/scala/caliban/schema/ArgBuilderSpec.scala index cea9fc2d8b..69d2733b5e 100644 --- a/core/src/test/scala/caliban/schema/ArgBuilderSpec.scala +++ b/core/src/test/scala/caliban/schema/ArgBuilderSpec.scala @@ -98,14 +98,13 @@ object ArgBuilderSpec extends ZIOSpecDefault { assertTrue(derivedAB.build(ObjectValue(Map("a" -> StringValue("x")))) == Right(Wrapper(SomeNullable("x")))) } ), - suite("oneOff") { - @GQLOneOfInput + suite("oneOf") { + @GQLOneOfInput("foo") sealed trait Foo object Foo { case class FooString(stringValue: String) extends Foo - - case class FooInt(intValue: Int) extends Foo + case class FooInt(intValue: Int) extends Foo } implicit val fooStringAb: ArgBuilder[Foo.FooString] = ArgBuilder.gen diff --git a/tools/src/main/scala/caliban/tools/ClientWriter.scala b/tools/src/main/scala/caliban/tools/ClientWriter.scala index 5ce83a8f9d..2f4f32d9c7 100644 --- a/tools/src/main/scala/caliban/tools/ClientWriter.scala +++ b/tools/src/main/scala/caliban/tools/ClientWriter.scala @@ -43,12 +43,12 @@ object ClientWriter { val mappingClashedTypeNames: Map[String, String] = getMappingsClashedNames( schema.definitions.collect { - case ObjectTypeDefinition(_, name, _, _, _) => name - case InputObjectTypeDefinition(_, name, _, _) => name - case EnumTypeDefinition(_, name, _, _) => name - case UnionTypeDefinition(_, name, _, _) => name - case ScalarTypeDefinition(_, name, _) => name - case InterfaceTypeDefinition(_, name, _, _, _) => name + case ObjectTypeDefinition(_, name, _, _, _) => name + case InputObjectTypeDefinition(_, name, _, _, _) => name + case EnumTypeDefinition(_, name, _, _) => name + case UnionTypeDefinition(_, name, _, _) => name + case ScalarTypeDefinition(_, name, _) => name + case InterfaceTypeDefinition(_, name, _, _, _) => name }, if (splitFiles) List("package") else Nil ) @@ -71,12 +71,12 @@ object ClientWriter { mappingClashedTypeNames.getOrElse(typeName, scalarMappingsWithDefaults.getOrElse(typeName, safeName(typeName))) val typesMap: Map[String, TypeDefinition] = schema.definitions.collect { - case op @ ObjectTypeDefinition(_, name, _, _, _) => name -> op - case op @ InputObjectTypeDefinition(_, name, _, _) => name -> op - case op @ EnumTypeDefinition(_, name, _, _) => name -> op - case op @ UnionTypeDefinition(_, name, _, _) => name -> op - case op @ ScalarTypeDefinition(_, name, _) => name -> op - case op @ InterfaceTypeDefinition(_, name, _, _, _) => name -> op + case op @ ObjectTypeDefinition(_, name, _, _, _) => name -> op + case op @ InputObjectTypeDefinition(_, name, _, _, _) => name -> op + case op @ EnumTypeDefinition(_, name, _, _) => name -> op + case op @ UnionTypeDefinition(_, name, _, _) => name -> op + case op @ ScalarTypeDefinition(_, name, _) => name -> op + case op @ InterfaceTypeDefinition(_, name, _, _, _) => name -> op }.map { case (name, op) => safeTypeName(name) -> op }.toMap diff --git a/tools/src/main/scala/caliban/tools/IntrospectionClient.scala b/tools/src/main/scala/caliban/tools/IntrospectionClient.scala index 9f59752896..4bd16b7773 100644 --- a/tools/src/main/scala/caliban/tools/IntrospectionClient.scala +++ b/tools/src/main/scala/caliban/tools/IntrospectionClient.scala @@ -96,7 +96,8 @@ object IntrospectionClient { inputFields: Option[List[InputValueDefinition]], interfaces: Option[List[Type]], enumValues: Option[List[EnumValueDefinition]], - possibleTypes: Option[List[Type]] + possibleTypes: Option[List[Type]], + isOneOf: Option[Boolean] ): Option[TypeDefinition] = kind match { case __TypeKind.SCALAR => Some(ScalarTypeDefinition(description, name.getOrElse(""), Nil)) @@ -134,7 +135,15 @@ object IntrospectionClient { case __TypeKind.ENUM => Some(EnumTypeDefinition(description, name.getOrElse(""), Nil, enumValues.getOrElse(Nil))) case __TypeKind.INPUT_OBJECT => - Some(InputObjectTypeDefinition(description, name.getOrElse(""), Nil, inputFields.getOrElse(Nil))) + Some( + InputObjectTypeDefinition( + description, + name.getOrElse(""), + Nil, + inputFields.getOrElse(Nil), + isOneOf.getOrElse(false) + ) + ) case __TypeKind.LIST | __TypeKind.NON_NULL => None } @@ -227,7 +236,8 @@ object IntrospectionClient { __EnumValue.isDeprecated ~ __EnumValue.deprecationReason).mapN(mapEnumValue _) } ~ - __Type.possibleTypes(typeRef)).mapN(mapType _) + __Type.possibleTypes(typeRef) ~ + __Type.isOneOf).mapN(mapType _) def introspection(supportIsRepeatable: Boolean): SelectionBuilder[RootQuery, Document] = Query.__schema { From 8732526524bb5e5a5d1f1ed5c02599cf5d01b8dc Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Mon, 21 Aug 2023 06:30:19 +0530 Subject: [PATCH 03/25] Add tests for executing queries with `oneOf` inputs --- .../caliban/schema/ArgBuilderDerivation.scala | 35 +++++++++++------- .../caliban/schema/SchemaDerivation.scala | 34 +++++++---------- .../caliban/schema/ArgBuilderDerivation.scala | 31 ++++++++++------ .../caliban/schema/SchemaDerivation.scala | 35 +++++++++++++++--- .../test/scala/caliban/RenderingSpec.scala | 2 +- .../caliban/execution/ExecutionSpec.scala | 37 ++++++++++++++++++- .../scala/caliban/schema/ArgBuilderSpec.scala | 11 +++--- 7 files changed, 125 insertions(+), 60 deletions(-) diff --git a/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala b/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala index cd0cd6d412..d3b07c8d16 100644 --- a/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala @@ -38,8 +38,10 @@ trait CommonArgBuilderDerivation { } def dispatch[T](ctx: SealedTrait[ArgBuilder, T]): ArgBuilder[T] = - if (ctx.annotations.collectFirst { case GQLOneOfInput(_) => () }.isDefined) makeOneOffBuilder(ctx) - else makeSumBuilder(ctx) + ctx.annotations.collectFirst { case GQLOneOfInput(name) => name } match { + case None => makeSumBuilder(ctx) + case Some(name) => makeOneOffBuilder(ctx, name) + } private def makeSumBuilder[T](ctx: SealedTrait[ArgBuilder, T]): ArgBuilder[T] = input => (input match { @@ -58,19 +60,24 @@ trait CommonArgBuilderDerivation { case None => Left(ExecutionError(s"Can't build a trait from input $input")) } - private def makeOneOffBuilder[A](ctx: SealedTrait[ArgBuilder, A]): ArgBuilder[A] = new ArgBuilder[A] { - private lazy val builders = ctx.subtypes.map(_.typeclass) - - def build(input: InputValue): Either[ExecutionError, A] = input match { - case InputValue.ObjectValue(value) if value.size == 1 => - builders.view - .map(_.build(input)) - .find(_.isRight) - .getOrElse(Left(ExecutionError(s"Invalid oneOf input $value for trait ${ctx.typeName.short}"))) - case InputValue.ObjectValue(_) => Left(ExecutionError("Exactly one key must be specified for oneOf inputs")) - case _ => Left(ExecutionError(s"Can't build a trait from input $input")) + private def makeOneOffBuilder[A](ctx: SealedTrait[ArgBuilder, A], oneOfInputName: String): ArgBuilder[A] = + new ArgBuilder[A] { + private lazy val builders = ctx.subtypes.map(_.typeclass) + + def build(input: InputValue): Either[ExecutionError, A] = input match { + case InputValue.ObjectValue(obj) => + obj.get(oneOfInputName) match { + case Some(inner @ InputValue.ObjectValue(fields)) if fields.sizeCompare(1) == 0 => + builders.view + .map(_.build(inner)) + .find(_.isRight) + .getOrElse(Left(ExecutionError(s"Invalid oneOf input $inner for trait ${ctx.typeName.short}}"))) + + case v => Left(ExecutionError(s"Exactly one key must be specified for oneOf inputs: $v")) + } + case _ => Left(ExecutionError(s"Can't build a trait from input $input")) + } } - } } diff --git a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala index 3b4529009f..fd1910a9bc 100644 --- a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala @@ -141,29 +141,23 @@ trait CommonSchemaDerivation[R] { Some(getDirectives(ctx.annotations)) ) } else if (isOneOffInputName.isDefined) { + val inner = makeInputObject( + Some(ctx.annotations.collectFirst { case GQLInputName(suffix) => suffix } + .getOrElse(customizeInputTypeName(getName(ctx)))), + getDescription(ctx), + ctx.subtypes + .flatMap(_.typeclass.toType_(isInput = true).inputFields.getOrElse(Nil)) + .toList + .map(_.nullable), + Some(ctx.typeName.full), + Some(List(Directive("oneOf"))), + isOneOf = true + ).nonNull + makeInputObject( None, None, - List( - __InputValue( - isOneOffInputName.getOrElse(""), - None, - () => - makeInputObject( - Some(ctx.annotations.collectFirst { case GQLInputName(suffix) => suffix } - .getOrElse(customizeInputTypeName(getName(ctx)))), - getDescription(ctx), - ctx.subtypes - .flatMap(_.typeclass.toType_(isInput = true).inputFields.getOrElse(Nil)) - .toList - .map(_.nullable), - Some(ctx.typeName.full), - Some(List(Directive("oneOf"))), - isOneOf = true - ).nonNull, - None - ) - ), + List(__InputValue(isOneOffInputName.getOrElse(""), None, () => inner, None)), Some(ctx.typeName.full), Some(List(Directive("oneOf"))), isOneOf = true diff --git a/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala b/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala index e81bddbbeb..4f6bd9fff6 100644 --- a/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala +++ b/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala @@ -4,7 +4,7 @@ import caliban.CalibanError.ExecutionError import caliban.{ schema, CalibanError, InputValue } import caliban.Value.* import caliban.schema.macros.Macros -import caliban.schema.Annotations.{ GQLDefault, GQLName } +import caliban.schema.Annotations.{ GQLDefault, GQLName, GQLOneOfInput } import scala.collection.mutable import scala.collection.mutable.ListBuffer @@ -49,7 +49,8 @@ trait CommonArgBuilderDerivation { inline if (Macros.isValidOneOffInput[A]) makeOneOffBuilder[A]( _recurse[A, m.MirroredElemLabels, m.MirroredElemTypes](), - constValue[m.MirroredLabel] + constValue[m.MirroredLabel], + Macros.annotations[A] ) else error( @@ -97,19 +98,25 @@ trait CommonArgBuilderDerivation { private def makeOneOffBuilder[A]( _subTypes: => List[(String, List[Any], ArgBuilder[Any])], - _traitLabel: => String + _traitLabel: => String, + _annotations: => List[Any] ): ArgBuilder[A] = new ArgBuilder[A] { - private lazy val builders = _subTypes.map(_._3).asInstanceOf[List[ArgBuilder[A]]] - private lazy val traitLabel = _traitLabel + private lazy val builders = _subTypes.map(_._3).asInstanceOf[List[ArgBuilder[A]]] + private lazy val traitLabel = _traitLabel + private lazy val oneOfInputName = _annotations.collectFirst { case GQLOneOfInput(name) => name }.getOrElse("") def build(input: InputValue): Either[ExecutionError, A] = input match { - case InputValue.ObjectValue(value) if value.sizeCompare(1) == 0 => - builders.view - .map(_.build(input)) - .find(_.isRight) - .getOrElse(Left(ExecutionError(s"Invalid oneOf input $value for trait $traitLabel"))) - case InputValue.ObjectValue(_) => Left(ExecutionError("Exactly one key must be specified for oneOf inputs")) - case _ => Left(ExecutionError(s"Can't build a trait from input $input")) + case InputValue.ObjectValue(obj) => + obj.get(oneOfInputName) match { + case Some(inner @ InputValue.ObjectValue(fields)) if fields.sizeCompare(1) == 0 => + builders.view + .map(_.build(inner)) + .find(_.isRight) + .getOrElse(Left(ExecutionError(s"Invalid oneOf input $inner for trait $traitLabel"))) + + case v => Left(ExecutionError(s"Exactly one key must be specified for oneOf inputs: $v")) + } + case _ => Left(ExecutionError(s"Can't build a trait from input $input")) } } diff --git a/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala index 77896ef1aa..870e3d267d 100644 --- a/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala @@ -92,11 +92,14 @@ trait CommonSchemaDerivation { annotations: List[Any] )(ordinal: A => Int): Schema[R, A] = new Schema[R, A] { - private lazy val members = _members + private lazy val members = _members.toVector // Vector has ~O(1) performance for `.apply` as opposed to List's O(n) + private lazy val membersOrdered = members.sortBy(_._1).toList - private lazy val subTypes = members.map { case (label, subTypeAnnotations, schema, _) => + private lazy val subTypes = membersOrdered.map { (label, subTypeAnnotations, schema, _) => (label, schema.toType_(), subTypeAnnotations) - }.sortBy { case (label, _, _) => label } + } + + private lazy val subtypesInput = membersOrdered.map(_._3.toType_(true)) private lazy val isEnum = subTypes.forall { (_, t, _) => t.fields(__DeprecatedArgs(Some(true))).forall(_.isEmpty) @@ -113,9 +116,29 @@ trait CommonSchemaDerivation { case _ => false } + private lazy val oneOfInputName = annotations.collectFirst { case GQLOneOfInput(name) => name } + def toType(isInput: Boolean, isSubscription: Boolean): __Type = if (!isInterface && !isUnion && subTypes.nonEmpty && isEnum) mkEnum(annotations, info, subTypes) - else if (!isInterface) + else if (oneOfInputName.nonEmpty) { + val inner = makeInputObject( + Some(getInputName(annotations).getOrElse(customizeInputTypeName(getName(annotations, info)))), + getDescription(annotations), + subtypesInput.flatMap(_.inputFields.getOrElse(Nil)).map(_.nullable), + Some(info.full), + Some(List(Directive("oneOf"))), + isOneOf = true + ).nonNull + + makeInputObject( + None, + None, + List(__InputValue(oneOfInputName.getOrElse(""), None, () => inner, None)), + Some(info.full), + Some(List(Directive("oneOf"))), + isOneOf = true + ) + } else if (!isInterface) makeUnion( Some(getName(annotations, info)), getDescription(annotations), @@ -233,13 +256,13 @@ trait CommonSchemaDerivation { makeEnum( Some(getName(annotations, info)), getDescription(annotations), - subTypes.collect { case (name, __Type(_, _, description, _, _, _, _, _, _, _, _, _), annotations) => + subTypes.collect { case (name, __Type(_, _, description, _, _, _, _, _, _, _, _, _, _), annotations) => __EnumValue( name, description, getDeprecatedReason(annotations).isDefined, getDeprecatedReason(annotations), - Some(annotations.collect { case GQLDirective(dir) => dir }.toList).filter(_.nonEmpty) + Some(annotations.collect { case GQLDirective(dir) => dir }).filter(_.nonEmpty) ) }, Some(info.full), diff --git a/core/src/test/scala/caliban/RenderingSpec.scala b/core/src/test/scala/caliban/RenderingSpec.scala index 49880959f4..5be1ae200c 100644 --- a/core/src/test/scala/caliban/RenderingSpec.scala +++ b/core/src/test/scala/caliban/RenderingSpec.scala @@ -173,7 +173,7 @@ object RenderingSpec extends ZIOSpecDefault { val api = graphQL(resolver) checkApi(api) }, - test("foo") { + test("@oneOf input") { @GQLOneOfInput("fooInput") sealed trait Foo diff --git a/core/src/test/scala/caliban/execution/ExecutionSpec.scala b/core/src/test/scala/caliban/execution/ExecutionSpec.scala index 6f854ba588..016bd743e3 100644 --- a/core/src/test/scala/caliban/execution/ExecutionSpec.scala +++ b/core/src/test/scala/caliban/execution/ExecutionSpec.scala @@ -1,14 +1,13 @@ package caliban.execution import java.util.UUID - import caliban.CalibanError.ExecutionError import caliban.Macros.gqldoc import caliban.TestUtils._ import caliban.Value.{ BooleanValue, IntValue, NullValue, StringValue } import caliban.introspection.adt.__Type import caliban.parsing.adt.LocationInfo -import caliban.schema.Annotations.{ GQLInterface, GQLName, GQLValueType } +import caliban.schema.Annotations.{ GQLInterface, GQLName, GQLOneOfInput, GQLValueType } import caliban.schema._ import caliban.schema.Schema.auto._ import caliban.schema.ArgBuilder.auto._ @@ -1248,6 +1247,40 @@ object ExecutionSpec extends ZIOSpecDefault { response.data.toString == """{"bases":[{"id":"1","name":"base 1","inner":[{"a":"a"}]},{"id":"2","name":"base 2","inner":[{"b":2}]}]}""" ) } + }, + test("oneOf inputs") { + @GQLOneOfInput("fooInput") + sealed trait Foo + + object Foo { + case class FooString(stringValue: String) extends Foo + case class FooInt(intValue: Int) extends Foo + } + + case class Queries(foo: Foo => String) + + implicit val fooStringAb: ArgBuilder[Foo.FooString] = ArgBuilder.gen + implicit val fooIntAb: ArgBuilder[Foo.FooInt] = ArgBuilder.gen + implicit val fooAb: ArgBuilder[Foo] = ArgBuilder.gen + implicit val schema: Schema[Any, Queries] = Schema.gen + + val api: GraphQL[Any] = graphQL(RootResolver(Queries { + case Foo.FooString(value) => value + case Foo.FooInt(value) => value.toString + })) + + val q1 = gqldoc("""{ foo(fooInput: {stringValue: "hello"}) }""") + val q2 = gqldoc("""{ foo(fooInput: {intValue: 42}) }""") + + for { + i <- api.interpreter + r1 <- i.execute(q1) + r2 <- i.execute(q2) + } yield assertTrue( + r1.data.toString == """{"foo":"hello"}""", + r2.data.toString == """{"foo":"42"}""" + ) + } ) } diff --git a/core/src/test/scala/caliban/schema/ArgBuilderSpec.scala b/core/src/test/scala/caliban/schema/ArgBuilderSpec.scala index 69d2733b5e..6de2d3315e 100644 --- a/core/src/test/scala/caliban/schema/ArgBuilderSpec.scala +++ b/core/src/test/scala/caliban/schema/ArgBuilderSpec.scala @@ -99,6 +99,7 @@ object ArgBuilderSpec extends ZIOSpecDefault { } ), suite("oneOf") { + @GQLOneOfInput("foo") sealed trait Foo @@ -111,13 +112,13 @@ object ArgBuilderSpec extends ZIOSpecDefault { implicit val fooIntAb: ArgBuilder[Foo.FooInt] = ArgBuilder.gen val fooAb: ArgBuilder[Foo] = ArgBuilder.gen + def mkInput(map: Map[String, InputValue]) = ObjectValue(Map("foo" -> ObjectValue(map))) + List( test("valid input") { assertTrue( - fooAb.build( - ObjectValue(Map("stringValue" -> StringValue("foo"))) - ) == Right(Foo.FooString("foo")), - fooAb.build(ObjectValue(Map("intValue" -> IntValue(42)))) == Right(Foo.FooInt(42)) + fooAb.build(mkInput(Map("stringValue" -> StringValue("foo")))) == Right(Foo.FooString("foo")), + fooAb.build(mkInput(Map("intValue" -> IntValue(42)))) == Right(Foo.FooInt(42)) ) }, test("invalid input") { @@ -127,7 +128,7 @@ object ArgBuilderSpec extends ZIOSpecDefault { Map("stringValue" -> NullValue), Map("stringValue" -> NullValue, "invalid" -> NullValue) ) - .map(input => assertTrue(fooAb.build(ObjectValue(input)).isLeft)) + .map(input => assertTrue(fooAb.build(mkInput(input)).isLeft)) .foldLeft(assertCompletes)(_ && _) } From 42b4d129568236f99687588428939887841ec3db Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Mon, 21 Aug 2023 10:17:33 +0300 Subject: [PATCH 04/25] Don't introspect `isOneOf` in `IntrospectionClient` --- .../caliban/client/IntrospectionClient.scala | 2 -- core/src/main/scala/caliban/Value.scala | 4 ++-- .../introspection/IntrospectionSpec.scala | 22 +++++++++++++++++++ .../caliban/tools/IntrospectionClient.scala | 16 +++----------- 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/client/src/main/scala/caliban/client/IntrospectionClient.scala b/client/src/main/scala/caliban/client/IntrospectionClient.scala index 9baa915594..abf7a76d42 100644 --- a/client/src/main/scala/caliban/client/IntrospectionClient.scala +++ b/client/src/main/scala/caliban/client/IntrospectionClient.scala @@ -153,8 +153,6 @@ object IntrospectionClient { Field("inputFields", OptionOf(ListOf(Obj(innerSelection)))) def ofType[A](innerSelection: SelectionBuilder[__Type, A]): SelectionBuilder[__Type, Option[A]] = Field("ofType", OptionOf(Obj(innerSelection))) - def isOneOf: SelectionBuilder[__Type, Option[Boolean]] = - Field("isOneOf", OptionOf(Scalar[Boolean]())) } type __Field diff --git a/core/src/main/scala/caliban/Value.scala b/core/src/main/scala/caliban/Value.scala index 006c95f70c..f43d9a61cb 100644 --- a/core/src/main/scala/caliban/Value.scala +++ b/core/src/main/scala/caliban/Value.scala @@ -66,10 +66,10 @@ object ResponseValue extends ValueJsonCompat { override def toString: String = fields.map { case (name, value) => s""""$name":${value.toString}""" }.mkString("{", ",", "}") - override def hashCode: Int = fields.toSet.hashCode() + override lazy val hashCode: Int = fields.toSet.hashCode() override def equals(other: Any): Boolean = other match { - case o: ObjectValue => o.hashCode() == hashCode + case o: ObjectValue => o.hashCode == hashCode case _ => false } } diff --git a/core/src/test/scala/caliban/introspection/IntrospectionSpec.scala b/core/src/test/scala/caliban/introspection/IntrospectionSpec.scala index 9009652b85..c97ac0ca58 100644 --- a/core/src/test/scala/caliban/introspection/IntrospectionSpec.scala +++ b/core/src/test/scala/caliban/introspection/IntrospectionSpec.scala @@ -195,6 +195,28 @@ object IntrospectionSpec extends ZIOSpecDefault { interpreter.flatMap(_.execute(query)).map { response => assertTrue(response.data.toString == """{"__type":null}""") } + }, + test("introspect oneOff types") { + val interpreter = graphQL(resolverOneOf).interpreter + + def query(name: String) = + s""" + query { + __type(name: "$name") { + name + isOneOf + } + } + """ + + for { + i <- interpreter + r1 <- i.execute(query("NameOrOriginInput")) + r2 <- i.execute(query("CharacterInput")) + } yield assertTrue( + r1.data.toString == """{"__type":{"name":"NameOrOriginInput","isOneOf":true}}""", + r2.data.toString == """{"__type":{"name":"CharacterInput","isOneOf":false}}""" + ) } ) } diff --git a/tools/src/main/scala/caliban/tools/IntrospectionClient.scala b/tools/src/main/scala/caliban/tools/IntrospectionClient.scala index 4bd16b7773..f5b38cabc2 100644 --- a/tools/src/main/scala/caliban/tools/IntrospectionClient.scala +++ b/tools/src/main/scala/caliban/tools/IntrospectionClient.scala @@ -96,8 +96,7 @@ object IntrospectionClient { inputFields: Option[List[InputValueDefinition]], interfaces: Option[List[Type]], enumValues: Option[List[EnumValueDefinition]], - possibleTypes: Option[List[Type]], - isOneOf: Option[Boolean] + possibleTypes: Option[List[Type]] ): Option[TypeDefinition] = kind match { case __TypeKind.SCALAR => Some(ScalarTypeDefinition(description, name.getOrElse(""), Nil)) @@ -135,15 +134,7 @@ object IntrospectionClient { case __TypeKind.ENUM => Some(EnumTypeDefinition(description, name.getOrElse(""), Nil, enumValues.getOrElse(Nil))) case __TypeKind.INPUT_OBJECT => - Some( - InputObjectTypeDefinition( - description, - name.getOrElse(""), - Nil, - inputFields.getOrElse(Nil), - isOneOf.getOrElse(false) - ) - ) + Some(InputObjectTypeDefinition(description, name.getOrElse(""), Nil, inputFields.getOrElse(Nil), isOneOf = false)) case __TypeKind.LIST | __TypeKind.NON_NULL => None } @@ -236,8 +227,7 @@ object IntrospectionClient { __EnumValue.isDeprecated ~ __EnumValue.deprecationReason).mapN(mapEnumValue _) } ~ - __Type.possibleTypes(typeRef) ~ - __Type.isOneOf).mapN(mapType _) + __Type.possibleTypes(typeRef)).mapN(mapType _) def introspection(supportIsRepeatable: Boolean): SelectionBuilder[RootQuery, Document] = Query.__schema { From 2ef9ed893c432fbca90bfeab085f86e95ef2b644 Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Mon, 21 Aug 2023 20:06:22 +0300 Subject: [PATCH 05/25] Add value type implementation --- .../caliban/schema/ArgBuilderDerivation.scala | 28 +++++----- .../caliban/schema/SchemaDerivation.scala | 30 +++++------ .../caliban/schema/ArgBuilderDerivation.scala | 31 +++++------ .../caliban/schema/SchemaDerivation.scala | 21 ++------ .../caliban/introspection/Introspector.scala | 2 +- .../caliban/introspection/adt/__Type.scala | 4 +- .../scala/caliban/schema/Annotations.scala | 2 +- .../main/scala/caliban/schema/Schema.scala | 24 ++++----- .../test/scala/caliban/RenderingSpec.scala | 51 +++++++++++++------ core/src/test/scala/caliban/TestUtils.scala | 10 ++-- .../caliban/execution/ExecutionSpec.scala | 32 +++++++++--- .../scala/caliban/schema/ArgBuilderSpec.scala | 11 ++-- 12 files changed, 129 insertions(+), 117 deletions(-) diff --git a/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala b/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala index d3b07c8d16..636395c041 100644 --- a/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala @@ -38,10 +38,8 @@ trait CommonArgBuilderDerivation { } def dispatch[T](ctx: SealedTrait[ArgBuilder, T]): ArgBuilder[T] = - ctx.annotations.collectFirst { case GQLOneOfInput(name) => name } match { - case None => makeSumBuilder(ctx) - case Some(name) => makeOneOffBuilder(ctx, name) - } + if (ctx.annotations.contains(GQLOneOfInput())) makeOneOffBuilder(ctx) + else makeSumBuilder(ctx) private def makeSumBuilder[T](ctx: SealedTrait[ArgBuilder, T]): ArgBuilder[T] = input => (input match { @@ -60,22 +58,20 @@ trait CommonArgBuilderDerivation { case None => Left(ExecutionError(s"Can't build a trait from input $input")) } - private def makeOneOffBuilder[A](ctx: SealedTrait[ArgBuilder, A], oneOfInputName: String): ArgBuilder[A] = + private def makeOneOffBuilder[A](ctx: SealedTrait[ArgBuilder, A]): ArgBuilder[A] = new ArgBuilder[A] { private lazy val builders = ctx.subtypes.map(_.typeclass) def build(input: InputValue): Either[ExecutionError, A] = input match { - case InputValue.ObjectValue(obj) => - obj.get(oneOfInputName) match { - case Some(inner @ InputValue.ObjectValue(fields)) if fields.sizeCompare(1) == 0 => - builders.view - .map(_.build(inner)) - .find(_.isRight) - .getOrElse(Left(ExecutionError(s"Invalid oneOf input $inner for trait ${ctx.typeName.short}}"))) - - case v => Left(ExecutionError(s"Exactly one key must be specified for oneOf inputs: $v")) - } - case _ => Left(ExecutionError(s"Can't build a trait from input $input")) + case InputValue.ObjectValue(fields) if fields.size == 1 => + builders.view + .map(_.build(input)) + .find(_.isRight) + .getOrElse(Left(ExecutionError(s"Invalid oneOf input $fields for trait ${ctx.typeName.short}}"))) + case InputValue.ObjectValue(_) => + Left(ExecutionError(s"Exactly one key must be specified for oneOf inputs")) + case _ => + Left(ExecutionError(s"Can't build a trait from input $input")) } } diff --git a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala index fd1910a9bc..5de4007571 100644 --- a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala @@ -98,8 +98,6 @@ trait CommonSchemaDerivation[R] { } def dispatch[T](ctx: SealedTrait[Typeclass, T]): Typeclass[T] = new Typeclass[T] { - private lazy val isOneOffInputName = ctx.annotations.collectFirst { case GQLOneOfInput(name) => name } - override def toType(isInput: Boolean, isSubscription: Boolean): __Type = { val subtypes = ctx.subtypes @@ -124,7 +122,15 @@ trait CommonSchemaDerivation[R] { case _ => false } - if (isEnum && subtypes.nonEmpty && !isInterface && !isUnion && isOneOffInputName.isEmpty) { + lazy val subtypeInputFields = + ctx.subtypes.map(_.typeclass.toType_(isInput = true).inputFields.getOrElse(Nil)).toList + + lazy val isOneOfInput = + ctx.annotations.contains(GQLOneOfInput()) && + subtypeInputFields.nonEmpty && + subtypeInputFields.forall(_.size == 1) + + if (isEnum && subtypes.nonEmpty && !isInterface && !isUnion && !isOneOfInput) { makeEnum( Some(getName(ctx)), getDescription(ctx), @@ -140,24 +146,12 @@ trait CommonSchemaDerivation[R] { Some(ctx.typeName.full), Some(getDirectives(ctx.annotations)) ) - } else if (isOneOffInputName.isDefined) { - val inner = makeInputObject( + } else if (isOneOfInput) { + makeInputObject( Some(ctx.annotations.collectFirst { case GQLInputName(suffix) => suffix } .getOrElse(customizeInputTypeName(getName(ctx)))), getDescription(ctx), - ctx.subtypes - .flatMap(_.typeclass.toType_(isInput = true).inputFields.getOrElse(Nil)) - .toList - .map(_.nullable), - Some(ctx.typeName.full), - Some(List(Directive("oneOf"))), - isOneOf = true - ).nonNull - - makeInputObject( - None, - None, - List(__InputValue(isOneOffInputName.getOrElse(""), None, () => inner, None)), + subtypeInputFields.flatten.map(_.nullable), Some(ctx.typeName.full), Some(List(Directive("oneOf"))), isOneOf = true diff --git a/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala b/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala index 4f6bd9fff6..b05d97e85a 100644 --- a/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala +++ b/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala @@ -49,8 +49,7 @@ trait CommonArgBuilderDerivation { inline if (Macros.isValidOneOffInput[A]) makeOneOffBuilder[A]( _recurse[A, m.MirroredElemLabels, m.MirroredElemTypes](), - constValue[m.MirroredLabel], - Macros.annotations[A] + constValue[m.MirroredLabel] ) else error( @@ -98,25 +97,21 @@ trait CommonArgBuilderDerivation { private def makeOneOffBuilder[A]( _subTypes: => List[(String, List[Any], ArgBuilder[Any])], - _traitLabel: => String, - _annotations: => List[Any] + _traitLabel: => String ): ArgBuilder[A] = new ArgBuilder[A] { - private lazy val builders = _subTypes.map(_._3).asInstanceOf[List[ArgBuilder[A]]] - private lazy val traitLabel = _traitLabel - private lazy val oneOfInputName = _annotations.collectFirst { case GQLOneOfInput(name) => name }.getOrElse("") + private lazy val builders = _subTypes.map(_._3).asInstanceOf[List[ArgBuilder[A]]] + private lazy val traitLabel = _traitLabel def build(input: InputValue): Either[ExecutionError, A] = input match { - case InputValue.ObjectValue(obj) => - obj.get(oneOfInputName) match { - case Some(inner @ InputValue.ObjectValue(fields)) if fields.sizeCompare(1) == 0 => - builders.view - .map(_.build(inner)) - .find(_.isRight) - .getOrElse(Left(ExecutionError(s"Invalid oneOf input $inner for trait $traitLabel"))) - - case v => Left(ExecutionError(s"Exactly one key must be specified for oneOf inputs: $v")) - } - case _ => Left(ExecutionError(s"Can't build a trait from input $input")) + case InputValue.ObjectValue(fields) if fields.size == 1 => + builders.view + .map(_.build(input)) + .find(_.isRight) + .getOrElse(Left(ExecutionError(s"Invalid oneOf input $fields for trait $traitLabel"))) + case InputValue.ObjectValue(_) => + Left(ExecutionError("Exactly one key must be specified for oneOf inputs")) + case _ => + Left(ExecutionError(s"Can't build a trait from input $input")) } } diff --git a/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala index 870e3d267d..03eb430db3 100644 --- a/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala @@ -99,8 +99,6 @@ trait CommonSchemaDerivation { (label, schema.toType_(), subTypeAnnotations) } - private lazy val subtypesInput = membersOrdered.map(_._3.toType_(true)) - private lazy val isEnum = subTypes.forall { (_, t, _) => t.fields(__DeprecatedArgs(Some(true))).forall(_.isEmpty) && t.inputFields.forall(_.isEmpty) @@ -116,24 +114,15 @@ trait CommonSchemaDerivation { case _ => false } - private lazy val oneOfInputName = annotations.collectFirst { case GQLOneOfInput(name) => name } + private lazy val isOneOfInput = annotations.contains(GQLOneOfInput()) def toType(isInput: Boolean, isSubscription: Boolean): __Type = - if (!isInterface && !isUnion && subTypes.nonEmpty && isEnum) mkEnum(annotations, info, subTypes) - else if (oneOfInputName.nonEmpty) { - val inner = makeInputObject( + if (!isInterface && !isUnion && subTypes.nonEmpty && isEnum && !isOneOfInput) mkEnum(annotations, info, subTypes) + else if (isOneOfInput) { + makeInputObject( Some(getInputName(annotations).getOrElse(customizeInputTypeName(getName(annotations, info)))), getDescription(annotations), - subtypesInput.flatMap(_.inputFields.getOrElse(Nil)).map(_.nullable), - Some(info.full), - Some(List(Directive("oneOf"))), - isOneOf = true - ).nonNull - - makeInputObject( - None, - None, - List(__InputValue(oneOfInputName.getOrElse(""), None, () => inner, None)), + membersOrdered.map(_._3.toType_(true)).flatMap(_.inputFields.getOrElse(Nil)), Some(info.full), Some(List(Directive("oneOf"))), isOneOf = true diff --git a/core/src/main/scala/caliban/introspection/Introspector.scala b/core/src/main/scala/caliban/introspection/Introspector.scala index 064b4a1317..0ff5fec209 100644 --- a/core/src/main/scala/caliban/introspection/Introspector.scala +++ b/core/src/main/scala/caliban/introspection/Introspector.scala @@ -79,7 +79,7 @@ object Introspector extends IntrospectionDerivation { .toList .sortBy(_.name.getOrElse("")) - val hasOneOf = types.exists(_.isOneOf.getOrElse(false)) + val hasOneOf = types.exists(_._isOneOfInput) val resolver = __Introspection( __Schema( diff --git a/core/src/main/scala/caliban/introspection/adt/__Type.scala b/core/src/main/scala/caliban/introspection/adt/__Type.scala index ec8354fb39..d50ccef004 100644 --- a/core/src/main/scala/caliban/introspection/adt/__Type.scala +++ b/core/src/main/scala/caliban/introspection/adt/__Type.scala @@ -107,7 +107,7 @@ case class __Type( name.getOrElse(""), directives.getOrElse(Nil), inputFields.getOrElse(Nil).map(_.toInputValueDefinition), - isOneOf.getOrElse(false) + _isOneOfInput ) ) case _ => None @@ -131,4 +131,6 @@ case class __Type( fields(__DeprecatedArgs(Some(true))).getOrElse(Nil) lazy val innerType: __Type = Types.innerType(this) + + def _isOneOfInput: Boolean = isOneOf.getOrElse(false) } diff --git a/core/src/main/scala/caliban/schema/Annotations.scala b/core/src/main/scala/caliban/schema/Annotations.scala index 240a63afc8..f98093e7f7 100644 --- a/core/src/main/scala/caliban/schema/Annotations.scala +++ b/core/src/main/scala/caliban/schema/Annotations.scala @@ -67,5 +67,5 @@ object Annotations { /** * Annotation to make a sealed trait as a GraphQL @oneOff input */ - case class GQLOneOfInput(fieldName: String) extends StaticAnnotation + case class GQLOneOfInput() extends StaticAnnotation } diff --git a/core/src/main/scala/caliban/schema/Schema.scala b/core/src/main/scala/caliban/schema/Schema.scala index 90565b40d0..248923ae65 100644 --- a/core/src/main/scala/caliban/schema/Schema.scala +++ b/core/src/main/scala/caliban/schema/Schema.scala @@ -391,19 +391,18 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { private lazy val inputType = ev1.toType_(true) private val unwrappedArgumentName = "value" - override lazy val arguments: List[__InputValue] = - inputType.inputFields.getOrElse( - handleInput(List.empty[__InputValue])( - List( - __InputValue( - unwrappedArgumentName, - None, - () => if (ev1.optional) inputType else inputType.nonNull, - None - ) - ) - ) + private def mkValueType = List( + __InputValue( + unwrappedArgumentName, + None, + () => if (ev1.optional) inputType else inputType.nonNull, + None ) + ) + + override lazy val arguments: List[__InputValue] = + if (inputType._isOneOfInput) mkValueType + else inputType.inputFields.getOrElse(handleInput(List.empty[__InputValue])(mkValueType)) override def optional: Boolean = ev2.optional override def toType(isInput: Boolean, isSubscription: Boolean): __Type = ev2.toType_(isInput, isSubscription) @@ -425,6 +424,7 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { case __TypeKind.SCALAR | __TypeKind.ENUM | __TypeKind.LIST => // argument was not wrapped in a case class onUnwrapped + case _ if inputType._isOneOfInput => onUnwrapped case _ => onWrapped } } diff --git a/core/src/test/scala/caliban/RenderingSpec.scala b/core/src/test/scala/caliban/RenderingSpec.scala index 5be1ae200c..ece6055e9c 100644 --- a/core/src/test/scala/caliban/RenderingSpec.scala +++ b/core/src/test/scala/caliban/RenderingSpec.scala @@ -173,25 +173,33 @@ object RenderingSpec extends ZIOSpecDefault { val api = graphQL(resolver) checkApi(api) }, - test("@oneOf input") { - - @GQLOneOfInput("fooInput") - sealed trait Foo + test("@oneOf input as value type") { + case class Queries(foo: Foo => String) - object Foo { - case class FooString(stringValue: String) extends Foo - case class FooInt(intValue: Int) extends Foo - } + implicit val schema: Schema[Any, Queries] = Schema.gen + val resolver = RootResolver(Queries(_.toString)) - case class Queries(foo: Foo => String) + val expected = + """schema { + | query: Queries + |} + | + |input FooInput @oneOf { + | intValue: Int + | stringValue: String + |} + | + |type Queries { + | foo(value: FooInput!): String! + |}""".stripMargin - implicit val fooIntAb: ArgBuilder[Foo.FooInt] = ArgBuilder.gen - implicit val fooStringAb: ArgBuilder[Foo.FooString] = ArgBuilder.gen - implicit val fooAb: ArgBuilder[Foo] = ArgBuilder.gen - implicit val fooSchema: Schema[Any, Foo] = Schema.gen - implicit val schema: Schema[Any, Queries] = Schema.gen + assertTrue(graphQL(resolver).render == expected) + }, + test("@oneOf input wrapped in a case class") { + case class Queries(foo: Foo.Wrapped => String) - val resolver = RootResolver(Queries(_.toString)) + implicit val schema: Schema[Any, Queries] = Schema.gen + val resolver = RootResolver(Queries(_.toString)) val expected = """schema { @@ -210,4 +218,17 @@ object RenderingSpec extends ZIOSpecDefault { assertTrue(graphQL(resolver).render == expected) } ) + + @GQLOneOfInput + sealed trait Foo + + object Foo { + case class FooString(stringValue: String) extends Foo + case class FooInt(intValue: Int) extends Foo + + case class Wrapped(fooInput: Foo) + } + + implicit val fooAb: ArgBuilder[Foo] = ArgBuilder.gen + implicit val fooSchema: Schema[Any, Foo] = Schema.gen } diff --git a/core/src/test/scala/caliban/TestUtils.scala b/core/src/test/scala/caliban/TestUtils.scala index 20e60fec02..7530a19bd2 100644 --- a/core/src/test/scala/caliban/TestUtils.scala +++ b/core/src/test/scala/caliban/TestUtils.scala @@ -103,11 +103,13 @@ object TestUtils { case class CharacterInArgs(@GQLDirective(Directive("lowercase")) names: List[String]) case class CharacterObjectArgs(character: CharacterInput) - @GQLOneOfInput("nameOrOrigin") + @GQLOneOfInput sealed trait NameOrOrigin object NameOrOrigin { case class ByName(name: String) extends NameOrOrigin case class ByOrigin(origin: Origin) extends NameOrOrigin + + case class Wrapper(nameOrOrigin: NameOrOrigin) } @GQLDescription("Queries") @@ -128,7 +130,7 @@ object TestUtils { @GQLDescription("Queries") case class QueryWithOneOf( exists: CharacterObjectArgs => Boolean, - character: NameOrOrigin => List[Character] + character: NameOrOrigin.Wrapper => List[Character] ) @GQLDescription("Mutations") @@ -162,8 +164,8 @@ object TestUtils { QueryWithOneOf( args => characters.exists(_.name == args.character.name), { - case NameOrOrigin.ByName(name) => characters.filter(c => c.name == name) - case NameOrOrigin.ByOrigin(origin) => characters.filter(c => c.origin == origin) + case NameOrOrigin.Wrapper(NameOrOrigin.ByName(name)) => characters.filter(c => c.name == name) + case NameOrOrigin.Wrapper(NameOrOrigin.ByOrigin(origin)) => characters.filter(c => c.origin == origin) } ) ) diff --git a/core/src/test/scala/caliban/execution/ExecutionSpec.scala b/core/src/test/scala/caliban/execution/ExecutionSpec.scala index 016bd743e3..39ba6a2876 100644 --- a/core/src/test/scala/caliban/execution/ExecutionSpec.scala +++ b/core/src/test/scala/caliban/execution/ExecutionSpec.scala @@ -1249,36 +1249,52 @@ object ExecutionSpec extends ZIOSpecDefault { } }, test("oneOf inputs") { - @GQLOneOfInput("fooInput") - sealed trait Foo + @GQLOneOfInput + sealed trait Foo object Foo { case class FooString(stringValue: String) extends Foo case class FooInt(intValue: Int) extends Foo + + case class Wrapper(fooInput: Foo) } - case class Queries(foo: Foo => String) + case class Queries(foo: Foo.Wrapper => String, fooUnwrapped: Foo => String) implicit val fooStringAb: ArgBuilder[Foo.FooString] = ArgBuilder.gen implicit val fooIntAb: ArgBuilder[Foo.FooInt] = ArgBuilder.gen implicit val fooAb: ArgBuilder[Foo] = ArgBuilder.gen implicit val schema: Schema[Any, Queries] = Schema.gen - val api: GraphQL[Any] = graphQL(RootResolver(Queries { - case Foo.FooString(value) => value - case Foo.FooInt(value) => value.toString - })) + val api: GraphQL[Any] = graphQL( + RootResolver( + Queries( + { + case Foo.Wrapper(Foo.FooString(value)) => value + case Foo.Wrapper(Foo.FooInt(value)) => value.toString + }, + { + case Foo.FooString(value) => value + case Foo.FooInt(value) => value.toString + } + ) + ) + ) val q1 = gqldoc("""{ foo(fooInput: {stringValue: "hello"}) }""") val q2 = gqldoc("""{ foo(fooInput: {intValue: 42}) }""") + val q3 = gqldoc("""{ fooUnwrapped(value: {intValue: 42}) }""") for { + _ <- api.validateRootSchema i <- api.interpreter r1 <- i.execute(q1) r2 <- i.execute(q2) + r3 <- i.execute(q3) } yield assertTrue( r1.data.toString == """{"foo":"hello"}""", - r2.data.toString == """{"foo":"42"}""" + r2.data.toString == """{"foo":"42"}""", + r3.data.toString == """{"fooUnwrapped":"42"}""" ) } diff --git a/core/src/test/scala/caliban/schema/ArgBuilderSpec.scala b/core/src/test/scala/caliban/schema/ArgBuilderSpec.scala index 6de2d3315e..6be97248b0 100644 --- a/core/src/test/scala/caliban/schema/ArgBuilderSpec.scala +++ b/core/src/test/scala/caliban/schema/ArgBuilderSpec.scala @@ -100,9 +100,8 @@ object ArgBuilderSpec extends ZIOSpecDefault { ), suite("oneOf") { - @GQLOneOfInput("foo") + @GQLOneOfInput sealed trait Foo - object Foo { case class FooString(stringValue: String) extends Foo case class FooInt(intValue: Int) extends Foo @@ -112,13 +111,11 @@ object ArgBuilderSpec extends ZIOSpecDefault { implicit val fooIntAb: ArgBuilder[Foo.FooInt] = ArgBuilder.gen val fooAb: ArgBuilder[Foo] = ArgBuilder.gen - def mkInput(map: Map[String, InputValue]) = ObjectValue(Map("foo" -> ObjectValue(map))) - List( test("valid input") { assertTrue( - fooAb.build(mkInput(Map("stringValue" -> StringValue("foo")))) == Right(Foo.FooString("foo")), - fooAb.build(mkInput(Map("intValue" -> IntValue(42)))) == Right(Foo.FooInt(42)) + fooAb.build(ObjectValue(Map("stringValue" -> StringValue("foo")))) == Right(Foo.FooString("foo")), + fooAb.build(ObjectValue(Map("intValue" -> IntValue(42)))) == Right(Foo.FooInt(42)) ) }, test("invalid input") { @@ -128,7 +125,7 @@ object ArgBuilderSpec extends ZIOSpecDefault { Map("stringValue" -> NullValue), Map("stringValue" -> NullValue, "invalid" -> NullValue) ) - .map(input => assertTrue(fooAb.build(mkInput(input)).isLeft)) + .map(input => assertTrue(fooAb.build(ObjectValue(input)).isLeft)) .foldLeft(assertCompletes)(_ && _) } From b42e03b6f6fd5115c49593b0f871208b6b28531e Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Mon, 21 Aug 2023 20:19:00 +0300 Subject: [PATCH 06/25] Rollback irrelevant changes --- .../caliban/schema/ArgBuilderDerivation.scala | 40 +++++++------------ .../caliban/schema/SchemaDerivation.scala | 38 +++++++----------- core/src/main/scala/caliban/Value.scala | 4 +- .../caliban/introspection/adt/__Type.scala | 3 +- .../caliban/parsing/adt/Definition.scala | 3 +- .../caliban/parsing/parsers/Parsers.scala | 3 +- .../scala/caliban/parsing/ParserSpec.scala | 6 +-- .../scala/caliban/tools/ClientWriter.scala | 24 +++++------ .../caliban/tools/IntrospectionClient.scala | 2 +- 9 files changed, 48 insertions(+), 75 deletions(-) diff --git a/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala b/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala index b05d97e85a..175394b874 100644 --- a/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala +++ b/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala @@ -4,41 +4,29 @@ import caliban.CalibanError.ExecutionError import caliban.{ schema, CalibanError, InputValue } import caliban.Value.* import caliban.schema.macros.Macros -import caliban.schema.Annotations.{ GQLDefault, GQLName, GQLOneOfInput } +import caliban.schema.Annotations.{ GQLDefault, GQLName } -import scala.collection.mutable -import scala.collection.mutable.ListBuffer import scala.deriving.Mirror import scala.compiletime.* import scala.util.NotGiven trait CommonArgBuilderDerivation { - - // For source compat inline def recurse[P, Label, A <: Tuple]( inline values: List[(String, List[Any], ArgBuilder[Any])] = Nil - ): List[(String, List[Any], ArgBuilder[Any])] = - _recurse[P, Label, A](ListBuffer.empty ++= values) - - private inline def _recurse[P, Label, A <: Tuple]( - inline values: ListBuffer[(String, List[Any], ArgBuilder[Any])] = - ListBuffer.empty[(String, List[Any], ArgBuilder[Any])] ): List[(String, List[Any], ArgBuilder[Any])] = inline erasedValue[(Label, A)] match { - case (_: EmptyTuple, _) => values.result() + case (_: EmptyTuple, _) => values.reverse case (_: (name *: names), _: (t *: ts)) => - _recurse[P, names, ts]( - values.addOne( - ( - constValue[name].toString, - Macros.annotations[t], { - inline if (Macros.isEnumField[P, t]) - inline if (!Macros.implicitExists[ArgBuilder[t]]) derived[t] - else summonInline[ArgBuilder[t]] + recurse[P, names, ts]( + ( + constValue[name].toString, + Macros.annotations[t], { + if (Macros.isEnumField[P, t]) + if (!Macros.implicitExists[ArgBuilder[t]]) derived[t] else summonInline[ArgBuilder[t]] - }.asInstanceOf[ArgBuilder[Any]] - ) - ) + else summonInline[ArgBuilder[t]] + }.asInstanceOf[ArgBuilder[Any]] + ) :: values ) } @@ -48,7 +36,7 @@ trait CommonArgBuilderDerivation { inline if (Macros.hasOneOfInputAnnotation[A]) { inline if (Macros.isValidOneOffInput[A]) makeOneOffBuilder[A]( - _recurse[A, m.MirroredElemLabels, m.MirroredElemTypes](), + recurse[A, m.MirroredElemLabels, m.MirroredElemTypes](), constValue[m.MirroredLabel] ) else @@ -57,13 +45,13 @@ trait CommonArgBuilderDerivation { ) } else makeSumArgBuilder[A]( - _recurse[A, m.MirroredElemLabels, m.MirroredElemTypes](), + recurse[A, m.MirroredElemLabels, m.MirroredElemTypes](), constValue[m.MirroredLabel] ) case m: Mirror.ProductOf[A] => makeProductArgBuilder( - _recurse[A, m.MirroredElemLabels, m.MirroredElemTypes](), + recurse[A, m.MirroredElemLabels, m.MirroredElemTypes](), Macros.paramAnnotations[A].to(Map) )(m.fromProduct) } diff --git a/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala index 03eb430db3..daee63f6be 100644 --- a/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala @@ -8,7 +8,6 @@ import caliban.schema.Step.ObjectStep import caliban.schema.Types.* import caliban.schema.macros.{ Macros, TypeInfo } -import scala.collection.mutable.ListBuffer import scala.compiletime.* import scala.deriving.Mirror import scala.util.NotGiven @@ -37,34 +36,25 @@ trait CommonSchemaDerivation { case _ => s"${name}Input" } - // For source compat inline def recurse[R, P, Label, A <: Tuple]( - inline values: List[(String, List[Any], Schema[R, Any], Int)] - )(inline index: Int = 0): List[(String, List[Any], Schema[R, Any], Int)] = - _recurse(ListBuffer.empty ++= values)(index) - - private inline def _recurse[R, P, Label, A <: Tuple]( - inline values: ListBuffer[(String, List[Any], Schema[R, Any], Int)] = - ListBuffer.empty[(String, List[Any], Schema[R, Any], Int)] + inline values: List[(String, List[Any], Schema[R, Any], Int)] = Nil )(inline index: Int = 0): List[(String, List[Any], Schema[R, Any], Int)] = inline erasedValue[(Label, A)] match { - case (_: EmptyTuple, _) => values.result() + case (_: EmptyTuple, _) => values.reverse case (_: (name *: names), _: (t *: ts)) => - _recurse[R, P, names, ts] { + recurse[R, P, names, ts] { inline if (Macros.isFieldExcluded[P, name]) values else - values.addOne( - ( - constValue[name].toString, - Macros.annotations[t], { - inline if (Macros.isEnumField[P, t]) - inline if (!Macros.implicitExists[Schema[R, t]]) derived[R, t] - else summonInline[Schema[R, t]] + ( + constValue[name].toString, + Macros.annotations[t], { + if (Macros.isEnumField[P, t]) + if (!Macros.implicitExists[Schema[R, t]]) derived[R, t] else summonInline[Schema[R, t]] - }.asInstanceOf[Schema[R, Any]], - index - ) - ) + else summonInline[Schema[R, t]] + }.asInstanceOf[Schema[R, Any]], + index + ) :: values }(index + 1) } @@ -72,14 +62,14 @@ trait CommonSchemaDerivation { inline summonInline[Mirror.Of[A]] match { case m: Mirror.SumOf[A] => makeSumSchema[R, A]( - _recurse[R, A, m.MirroredElemLabels, m.MirroredElemTypes]()(), + recurse[R, A, m.MirroredElemLabels, m.MirroredElemTypes]()(), Macros.typeInfo[A], Macros.annotations[A] )(m.ordinal) case m: Mirror.ProductOf[A] => makeProductSchema[R, A]( - _recurse[R, A, m.MirroredElemLabels, m.MirroredElemTypes]()(), + recurse[R, A, m.MirroredElemLabels, m.MirroredElemTypes]()(), Macros.typeInfo[A], Macros.annotations[A], Macros.paramAnnotations[A].toMap diff --git a/core/src/main/scala/caliban/Value.scala b/core/src/main/scala/caliban/Value.scala index f43d9a61cb..006c95f70c 100644 --- a/core/src/main/scala/caliban/Value.scala +++ b/core/src/main/scala/caliban/Value.scala @@ -66,10 +66,10 @@ object ResponseValue extends ValueJsonCompat { override def toString: String = fields.map { case (name, value) => s""""$name":${value.toString}""" }.mkString("{", ",", "}") - override lazy val hashCode: Int = fields.toSet.hashCode() + override def hashCode: Int = fields.toSet.hashCode() override def equals(other: Any): Boolean = other match { - case o: ObjectValue => o.hashCode == hashCode + case o: ObjectValue => o.hashCode() == hashCode case _ => false } } diff --git a/core/src/main/scala/caliban/introspection/adt/__Type.scala b/core/src/main/scala/caliban/introspection/adt/__Type.scala index d50ccef004..36fce00e7c 100644 --- a/core/src/main/scala/caliban/introspection/adt/__Type.scala +++ b/core/src/main/scala/caliban/introspection/adt/__Type.scala @@ -106,8 +106,7 @@ case class __Type( description, name.getOrElse(""), directives.getOrElse(Nil), - inputFields.getOrElse(Nil).map(_.toInputValueDefinition), - _isOneOfInput + inputFields.getOrElse(Nil).map(_.toInputValueDefinition) ) ) case _ => None diff --git a/core/src/main/scala/caliban/parsing/adt/Definition.scala b/core/src/main/scala/caliban/parsing/adt/Definition.scala index 5d15865c64..0bace1caa3 100644 --- a/core/src/main/scala/caliban/parsing/adt/Definition.scala +++ b/core/src/main/scala/caliban/parsing/adt/Definition.scala @@ -108,8 +108,7 @@ object Definition { description: Option[String], name: String, directives: List[Directive], - fields: List[InputValueDefinition], - isOneOf: Boolean + fields: List[InputValueDefinition] ) extends TypeDefinition { override def toString: String = "Input Object" } diff --git a/core/src/main/scala/caliban/parsing/parsers/Parsers.scala b/core/src/main/scala/caliban/parsing/parsers/Parsers.scala index 1dc5eb2ac5..5e6b158f37 100644 --- a/core/src/main/scala/caliban/parsing/parsers/Parsers.scala +++ b/core/src/main/scala/caliban/parsing/parsers/Parsers.scala @@ -92,8 +92,7 @@ object Parsers extends SelectionParsers { description.map(_.value), name, directives = directives.getOrElse(Nil), - fields = fields.fold(List[InputValueDefinition]())(_.toList), - isOneOf = directives.fold(false)(_.exists(_.name == "oneOf")) + fields = fields.fold(List[InputValueDefinition]())(_.toList) ) } diff --git a/core/src/test/scala/caliban/parsing/ParserSpec.scala b/core/src/test/scala/caliban/parsing/ParserSpec.scala index b43d7aacd6..ac91a90321 100644 --- a/core/src/test/scala/caliban/parsing/ParserSpec.scala +++ b/core/src/test/scala/caliban/parsing/ParserSpec.scala @@ -553,8 +553,7 @@ object ParserSpec extends ZIOSpecDefault { description = None, name = "BarBaz", directives = Nil, - fields = Nil, - isOneOf = false + fields = Nil ) ), sourceMapper = SourceMapper.apply(inputWithNoBody) @@ -572,8 +571,7 @@ object ParserSpec extends ZIOSpecDefault { description = None, name = "BarBaz", directives = Nil, - fields = Nil, - isOneOf = false + fields = Nil ) ), sourceMapper = SourceMapper.apply(inputWithEmptyBody) diff --git a/tools/src/main/scala/caliban/tools/ClientWriter.scala b/tools/src/main/scala/caliban/tools/ClientWriter.scala index 2f4f32d9c7..5ce83a8f9d 100644 --- a/tools/src/main/scala/caliban/tools/ClientWriter.scala +++ b/tools/src/main/scala/caliban/tools/ClientWriter.scala @@ -43,12 +43,12 @@ object ClientWriter { val mappingClashedTypeNames: Map[String, String] = getMappingsClashedNames( schema.definitions.collect { - case ObjectTypeDefinition(_, name, _, _, _) => name - case InputObjectTypeDefinition(_, name, _, _, _) => name - case EnumTypeDefinition(_, name, _, _) => name - case UnionTypeDefinition(_, name, _, _) => name - case ScalarTypeDefinition(_, name, _) => name - case InterfaceTypeDefinition(_, name, _, _, _) => name + case ObjectTypeDefinition(_, name, _, _, _) => name + case InputObjectTypeDefinition(_, name, _, _) => name + case EnumTypeDefinition(_, name, _, _) => name + case UnionTypeDefinition(_, name, _, _) => name + case ScalarTypeDefinition(_, name, _) => name + case InterfaceTypeDefinition(_, name, _, _, _) => name }, if (splitFiles) List("package") else Nil ) @@ -71,12 +71,12 @@ object ClientWriter { mappingClashedTypeNames.getOrElse(typeName, scalarMappingsWithDefaults.getOrElse(typeName, safeName(typeName))) val typesMap: Map[String, TypeDefinition] = schema.definitions.collect { - case op @ ObjectTypeDefinition(_, name, _, _, _) => name -> op - case op @ InputObjectTypeDefinition(_, name, _, _, _) => name -> op - case op @ EnumTypeDefinition(_, name, _, _) => name -> op - case op @ UnionTypeDefinition(_, name, _, _) => name -> op - case op @ ScalarTypeDefinition(_, name, _) => name -> op - case op @ InterfaceTypeDefinition(_, name, _, _, _) => name -> op + case op @ ObjectTypeDefinition(_, name, _, _, _) => name -> op + case op @ InputObjectTypeDefinition(_, name, _, _) => name -> op + case op @ EnumTypeDefinition(_, name, _, _) => name -> op + case op @ UnionTypeDefinition(_, name, _, _) => name -> op + case op @ ScalarTypeDefinition(_, name, _) => name -> op + case op @ InterfaceTypeDefinition(_, name, _, _, _) => name -> op }.map { case (name, op) => safeTypeName(name) -> op }.toMap diff --git a/tools/src/main/scala/caliban/tools/IntrospectionClient.scala b/tools/src/main/scala/caliban/tools/IntrospectionClient.scala index f5b38cabc2..9f59752896 100644 --- a/tools/src/main/scala/caliban/tools/IntrospectionClient.scala +++ b/tools/src/main/scala/caliban/tools/IntrospectionClient.scala @@ -134,7 +134,7 @@ object IntrospectionClient { case __TypeKind.ENUM => Some(EnumTypeDefinition(description, name.getOrElse(""), Nil, enumValues.getOrElse(Nil))) case __TypeKind.INPUT_OBJECT => - Some(InputObjectTypeDefinition(description, name.getOrElse(""), Nil, inputFields.getOrElse(Nil), isOneOf = false)) + Some(InputObjectTypeDefinition(description, name.getOrElse(""), Nil, inputFields.getOrElse(Nil))) case __TypeKind.LIST | __TypeKind.NON_NULL => None } From 94bfc92275c6645381dbcc0c41ba30da796bd8ea Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Mon, 21 Aug 2023 20:37:59 +0300 Subject: [PATCH 07/25] Fix Scala 3 derivation --- core/src/main/scala-3/caliban/schema/SchemaDerivation.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala index daee63f6be..9a751816d0 100644 --- a/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala @@ -112,7 +112,7 @@ trait CommonSchemaDerivation { makeInputObject( Some(getInputName(annotations).getOrElse(customizeInputTypeName(getName(annotations, info)))), getDescription(annotations), - membersOrdered.map(_._3.toType_(true)).flatMap(_.inputFields.getOrElse(Nil)), + membersOrdered.map(_._3.toType_(true)).flatMap(_.inputFields.getOrElse(Nil)).map(_.nullable), Some(info.full), Some(List(Directive("oneOf"))), isOneOf = true From aa8cc9b0478986a65ca321ce4997a78797cdb447 Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Fri, 25 Aug 2023 19:39:42 +0300 Subject: [PATCH 08/25] Fix merge errors --- core/src/main/scala/caliban/rendering/DocumentRenderer.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/scala/caliban/rendering/DocumentRenderer.scala b/core/src/main/scala/caliban/rendering/DocumentRenderer.scala index 8ffc97e6d5..1a7b8d35b5 100644 --- a/core/src/main/scala/caliban/rendering/DocumentRenderer.scala +++ b/core/src/main/scala/caliban/rendering/DocumentRenderer.scala @@ -437,16 +437,17 @@ object DocumentRenderer extends Renderer[Document] { value match { case InputObjectTypeDefinition(description, name, directives, fields) => newlineOrSpace(indent, write) + newline(indent, write) descriptionRenderer.unsafeRender(description, indent, write) write append "input " write append name directivesRenderer.unsafeRender(directives, indent, write) space(indent, write) write append '{' + newline(indent, write) fieldsRenderer.unsafeRender(fields, increment(indent), write) newline(indent, write) write append '}' - newline(indent, write) } } From 9018352f9cd5908e0b3cde88bea7cbf4ca20cdcd Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Sun, 27 Aug 2023 11:09:01 +0300 Subject: [PATCH 09/25] PR comments --- .../caliban/schema/ArgBuilderDerivation.scala | 32 ++++-- .../caliban/schema/SchemaDerivation.scala | 20 ++-- .../caliban/schema/ArgBuilderDerivation.scala | 42 ++++--- .../caliban/schema/SchemaDerivation.scala | 46 +++++--- .../caliban/schema/macros/Macros.scala | 19 ---- .../scala/caliban/schema/Annotations.scala | 7 +- .../src/main/scala/caliban/schema/Types.scala | 8 ++ .../test/scala/caliban/RenderingSpec.scala | 103 +++++++++--------- core/src/test/scala/caliban/TestUtils.scala | 8 +- .../caliban/execution/ExecutionSpec.scala | 10 +- .../introspection/IntrospectionSpec.scala | 4 +- .../scala/caliban/schema/ArgBuilderSpec.scala | 40 +++++-- 12 files changed, 204 insertions(+), 135 deletions(-) diff --git a/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala b/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala index 636395c041..8f5f766497 100644 --- a/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala @@ -3,7 +3,7 @@ package caliban.schema import caliban.CalibanError.ExecutionError import caliban.InputValue import caliban.Value._ -import caliban.schema.Annotations.{ GQLDefault, GQLName, GQLOneOfInput } +import caliban.schema.Annotations.{ GQLDefault, GQLName, GQLOneOfInput, GQLOneOfInputName, GQLValueType } import magnolia._ import mercator.Monadic @@ -38,7 +38,7 @@ trait CommonArgBuilderDerivation { } def dispatch[T](ctx: SealedTrait[ArgBuilder, T]): ArgBuilder[T] = - if (ctx.annotations.contains(GQLOneOfInput())) makeOneOffBuilder(ctx) + if (ctx.annotations.contains(GQLOneOfInput())) makeOneOfBuilder(ctx) else makeSumBuilder(ctx) private def makeSumBuilder[T](ctx: SealedTrait[ArgBuilder, T]): ArgBuilder[T] = input => @@ -58,18 +58,32 @@ trait CommonArgBuilderDerivation { case None => Left(ExecutionError(s"Can't build a trait from input $input")) } - private def makeOneOffBuilder[A](ctx: SealedTrait[ArgBuilder, A]): ArgBuilder[A] = + private def makeOneOfBuilder[A](ctx: SealedTrait[ArgBuilder, A]): ArgBuilder[A] = new ArgBuilder[A] { - private lazy val builders = ctx.subtypes.map(_.typeclass) + + private val builders = ctx.subtypes.map { p => + val name = Types.oneOfInputFieldName(p.typeName.short, p.annotations) + val isValueType = p.annotations.exists { case GQLValueType(_) => true; case _ => false } + + name -> (p.typeclass, isValueType) + }.toMap def build(input: InputValue): Either[ExecutionError, A] = input match { case InputValue.ObjectValue(fields) if fields.size == 1 => - builders.view - .map(_.build(input)) - .find(_.isRight) - .getOrElse(Left(ExecutionError(s"Invalid oneOf input $fields for trait ${ctx.typeName.short}}"))) + val (key, innerValue) = fields.head + builders + .get(key) + .toRight(ExecutionError(s"Invalid oneOf input $fields for trait ${ctx.typeName.short}}")) + .flatMap { + case (builder, true) => builder.build(innerValue) + case (builder, _) => + innerValue match { + case _: InputValue.ObjectValue => builder.build(innerValue) + case _ => Left(ExecutionError(s"Can't build a trait from input $input")) + } + } case InputValue.ObjectValue(_) => - Left(ExecutionError(s"Exactly one key must be specified for oneOf inputs")) + Left(ExecutionError("Exactly one key must be specified for oneOf inputs")) case _ => Left(ExecutionError(s"Can't build a trait from input $input")) } diff --git a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala index 75fc667a40..20fc290043 100644 --- a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala @@ -130,13 +130,7 @@ trait CommonSchemaDerivation[R] { case _ => false } - lazy val subtypeInputFields = - ctx.subtypes.map(_.typeclass.toType_(isInput = true).inputFields.getOrElse(Nil)).toList - - lazy val isOneOfInput = - ctx.annotations.contains(GQLOneOfInput()) && - subtypeInputFields.nonEmpty && - subtypeInputFields.forall(_.size == 1) + val isOneOfInput = ctx.annotations.contains(GQLOneOfInput()) if (isEnum && subtypes.nonEmpty && !isInterface && !isUnion && !isOneOfInput) { makeEnum( @@ -154,12 +148,20 @@ trait CommonSchemaDerivation[R] { Some(ctx.typeName.full), Some(getDirectives(ctx.annotations)) ) - } else if (isOneOfInput) { + } else if (isOneOfInput && isInput) { makeInputObject( Some(ctx.annotations.collectFirst { case GQLInputName(suffix) => suffix } .getOrElse(customizeInputTypeName(getName(ctx)))), getDescription(ctx), - subtypeInputFields.flatten.map(_.nullable), + ctx.subtypes.toList.map { p => + __InputValue( + oneOfInputFieldName(p.typeName.short, p.annotations), + getDescription(p.annotations), + () => p.typeclass.toType_(isInput = true), + None, + None + ) + }, Some(ctx.typeName.full), Some(List(Directive("oneOf"))), isOneOf = true diff --git a/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala b/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala index 175394b874..32e23727e5 100644 --- a/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala +++ b/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala @@ -4,7 +4,7 @@ import caliban.CalibanError.ExecutionError import caliban.{ schema, CalibanError, InputValue } import caliban.Value.* import caliban.schema.macros.Macros -import caliban.schema.Annotations.{ GQLDefault, GQLName } +import caliban.schema.Annotations.{ GQLDefault, GQLName, GQLOneOfInputName, GQLValueType } import scala.deriving.Mirror import scala.compiletime.* @@ -34,15 +34,10 @@ trait CommonArgBuilderDerivation { inline summonInline[Mirror.Of[A]] match { case m: Mirror.SumOf[A] => inline if (Macros.hasOneOfInputAnnotation[A]) { - inline if (Macros.isValidOneOffInput[A]) - makeOneOffBuilder[A]( - recurse[A, m.MirroredElemLabels, m.MirroredElemTypes](), - constValue[m.MirroredLabel] - ) - else - error( - "Invalid oneOf input. OneOff inputs must be sealed traits with 2 or more case classes extending them that:\n\t1. Have a single non-nullable field\n\t2. Do not have duplicated field names\n\t" - ) + makeOneOfBuilder[A]( + recurse[A, m.MirroredElemLabels, m.MirroredElemTypes](), + constValue[m.MirroredLabel] + ) } else makeSumArgBuilder[A]( recurse[A, m.MirroredElemLabels, m.MirroredElemTypes](), @@ -83,19 +78,34 @@ trait CommonArgBuilderDerivation { } } - private def makeOneOffBuilder[A]( + private def makeOneOfBuilder[A]( _subTypes: => List[(String, List[Any], ArgBuilder[Any])], _traitLabel: => String ): ArgBuilder[A] = new ArgBuilder[A] { - private lazy val builders = _subTypes.map(_._3).asInstanceOf[List[ArgBuilder[A]]] + + private val builders = _subTypes.map { (label, annotations, argBuilder) => + val name = Types.oneOfInputFieldName(label, annotations) + val isValueType = annotations.exists { case GQLValueType(_) => true; case _ => false } + + name -> (argBuilder, isValueType) + }.toMap.asInstanceOf[Map[String, (ArgBuilder[A], Boolean)]] + private lazy val traitLabel = _traitLabel def build(input: InputValue): Either[ExecutionError, A] = input match { case InputValue.ObjectValue(fields) if fields.size == 1 => - builders.view - .map(_.build(input)) - .find(_.isRight) - .getOrElse(Left(ExecutionError(s"Invalid oneOf input $fields for trait $traitLabel"))) + val (key, innerValue) = fields.head + builders + .get(key) + .toRight(ExecutionError(s"Invalid oneOf input $fields for trait $traitLabel")) + .flatMap { + case (builder, true) => builder.build(innerValue) + case (builder, _) => + innerValue match { + case _: InputValue.ObjectValue => builder.build(innerValue) + case _ => Left(ExecutionError(s"Can't build a trait from input $input")) + } + } case InputValue.ObjectValue(_) => Left(ExecutionError("Exactly one key must be specified for oneOf inputs")) case _ => diff --git a/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala index 8298b7b437..628dea5d27 100644 --- a/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala @@ -82,10 +82,12 @@ trait CommonSchemaDerivation { annotations: List[Any] )(ordinal: A => Int): Schema[R, A] = new Schema[R, A] { - private lazy val members = _members.toVector // Vector has ~O(1) performance for `.apply` as opposed to List's O(n) + // Vector has ~O(1) performance for `.apply` as opposed to List's O(n) + private lazy val members = _members.map(v => (v._1, v._2, v._3)).toVector + private lazy val membersOrdered = members.sortBy(_._1).toList - private lazy val subTypes = membersOrdered.map { (label, subTypeAnnotations, schema, _) => + private lazy val subTypes = membersOrdered.map { (label, subTypeAnnotations, schema) => (label, schema.toType_(), subTypeAnnotations) } @@ -107,16 +109,10 @@ trait CommonSchemaDerivation { private lazy val isOneOfInput = annotations.contains(GQLOneOfInput()) def toType(isInput: Boolean, isSubscription: Boolean): __Type = - if (!isInterface && !isUnion && subTypes.nonEmpty && isEnum && !isOneOfInput) mkEnum(annotations, info, subTypes) - else if (isOneOfInput) { - makeInputObject( - Some(getInputName(annotations).getOrElse(customizeInputTypeName(getName(annotations, info)))), - getDescription(annotations), - membersOrdered.map(_._3.toType_(true)).flatMap(_.inputFields.getOrElse(Nil)).map(_.nullable), - Some(info.full), - Some(List(Directive("oneOf"))), - isOneOf = true - ) + if (!isInterface && !isUnion && membersOrdered.nonEmpty && isEnum && !isOneOfInput) + mkEnum(annotations, info, subTypes) + else if (isOneOfInput && isInput) { + mkOneOfInput(annotations, membersOrdered, info) } else if (!isInterface) makeUnion( Some(getName(annotations, info)), @@ -131,7 +127,7 @@ trait CommonSchemaDerivation { } def resolve(value: A): Step[R] = { - val (label, _, schema, _) = members(ordinal(value)) + val (label, _, schema) = members(ordinal(value)) if (isEnum) PureStep(EnumValue(label)) else schema.resolve(value) } } @@ -175,7 +171,7 @@ trait CommonSchemaDerivation { head._3.resolve(value.asInstanceOf[Product].productElement(head._4)) } else { val fieldsBuilder = Map.newBuilder[String, Step[R]] - fields.foreach { case (label, _, schema, index) => + fields.foreach { (label, _, schema, index) => val fieldAnnotations = paramAnnotations.getOrElse(label, Nil) lazy val step = schema.resolve(value.asInstanceOf[Product].productElement(index)) fieldsBuilder += getName(fieldAnnotations, label) -> { @@ -304,6 +300,28 @@ trait CommonSchemaDerivation { Some(getDirectives(annotations)) ) + private def mkOneOfInput[R]( + annotations: List[Any], + members: List[(String, List[Any], Schema[R, Any])], + info: TypeInfo + ) = + makeInputObject( + Some(getInputName(annotations).getOrElse(customizeInputTypeName(getName(annotations, info)))), + getDescription(annotations), + members.map { (label, annotations, schema) => + __InputValue( + oneOfInputFieldName(label, annotations), + getDescription(annotations), + () => schema.toType_(isInput = true), + None, + None + ) + }, + Some(info.full), + Some(List(Directive("oneOf"))), + isOneOf = true + ) + private def mkObject[R]( annotations: List[Any], fields: List[(String, List[Any], Schema[R, Any], Int)], diff --git a/core/src/main/scala-3/caliban/schema/macros/Macros.scala b/core/src/main/scala-3/caliban/schema/macros/Macros.scala index 5bb365106e..32c40e7933 100644 --- a/core/src/main/scala-3/caliban/schema/macros/Macros.scala +++ b/core/src/main/scala-3/caliban/schema/macros/Macros.scala @@ -15,7 +15,6 @@ private[caliban] object Macros { inline def isEnumField[P, T]: Boolean = ${ isEnumFieldImpl[P, T] } inline def implicitExists[T]: Boolean = ${ implicitExistsImpl[T] } inline def hasOneOfInputAnnotation[P]: Boolean = ${ hasOneOfInputAnnotationImpl[P] } - inline def isValidOneOffInput[P]: Boolean = ${ isValidOneOffInputImpl[P] } def annotationsImpl[T: Type](using qctx: Quotes): Expr[List[Any]] = { import qctx.reflect.* @@ -93,22 +92,4 @@ private[caliban] object Macros { import q.reflect.* Expr(TypeRepr.of[T].typeSymbol.annotations.exists(_.tpe.typeSymbol.name == "GQLOneOfInput")) } - - def isValidOneOffInputImpl[T: Type](using q: Quotes): Expr[Boolean] = { - import q.reflect.* - val tpe = TypeRepr.of[T].typeSymbol - val flags = tpe.flags - if (flags.is(Flags.Sealed) && flags.is(Flags.Trait)) { - val constructors = tpe.children.map(_.primaryConstructor) - val children = constructors.map(_.paramSymss.flatten.map(_.name)) - val size = children.size - Expr( - size >= 2 - && children.forall(_.size == 1) - && size == children.flatten.distinct.size - && !constructors.exists(_.signature.paramSigs.contains("scala.Option")) - ) - } else Expr(false) - } - } diff --git a/core/src/main/scala/caliban/schema/Annotations.scala b/core/src/main/scala/caliban/schema/Annotations.scala index f98093e7f7..ec0952b29d 100644 --- a/core/src/main/scala/caliban/schema/Annotations.scala +++ b/core/src/main/scala/caliban/schema/Annotations.scala @@ -65,7 +65,12 @@ object Annotations { case class GQLDefault(value: String) extends StaticAnnotation /** - * Annotation to make a sealed trait as a GraphQL @oneOff input + * Annotation to make a sealed trait as a GraphQL @oneOf input */ case class GQLOneOfInput() extends StaticAnnotation + + /** + * Annotation used to rename the field name of a @oneOf input + */ + case class GQLOneOfInputName(value: String) extends StaticAnnotation } diff --git a/core/src/main/scala/caliban/schema/Types.scala b/core/src/main/scala/caliban/schema/Types.scala index dbb8620809..b3a766430b 100644 --- a/core/src/main/scala/caliban/schema/Types.scala +++ b/core/src/main/scala/caliban/schema/Types.scala @@ -2,6 +2,7 @@ package caliban.schema import caliban.introspection.adt._ import caliban.parsing.adt.Directive +import caliban.schema.Annotations.GQLOneOfInputName import scala.annotation.tailrec @@ -213,4 +214,11 @@ object Types { case __TypeKind.NON_NULL => t.ofType.map(name) case _ => t.name }).getOrElse("") + + private[caliban] def oneOfInputFieldName(typeName: String, annotations: Seq[Any]): String = + annotations.collectFirst { case GQLOneOfInputName(name) => name }.getOrElse { + val s = typeName.toCharArray + s(0) = s(0).toLower + new String(s) + } } diff --git a/core/src/test/scala/caliban/RenderingSpec.scala b/core/src/test/scala/caliban/RenderingSpec.scala index 0862b59893..f5d2b2a451 100644 --- a/core/src/test/scala/caliban/RenderingSpec.scala +++ b/core/src/test/scala/caliban/RenderingSpec.scala @@ -2,23 +2,22 @@ package caliban import caliban.CalibanError.ParsingError import caliban.TestUtils._ -import caliban.introspection.adt.{ __Directive, __DirectiveLocation, __Type, __TypeKind } +import caliban.introspection.adt.{ __Type, __TypeKind } import caliban.parsing.Parser -import caliban.parsing.adt.Definition.{ TypeSystemDefinition, TypeSystemExtension } import caliban.parsing.adt.Definition.TypeSystemDefinition.TypeDefinition import caliban.parsing.adt.Definition.TypeSystemDefinition.TypeDefinition.{ EnumValueDefinition, FieldDefinition, InputValueDefinition } +import caliban.parsing.adt.Definition.{ TypeSystemDefinition, TypeSystemExtension } import caliban.parsing.adt.{ Definition, Directive } import caliban.rendering.DocumentRenderer -import caliban.schema.Annotations.GQLOneOfInput -import caliban.schema.{ ArgBuilder, Schema } +import caliban.schema.Annotations.{ GQLOneOfInput, GQLOneOfInputName, GQLValueType } import caliban.schema.Schema.auto._ import caliban.schema.ArgBuilder.auto._ +import caliban.schema.{ ArgBuilder, Schema } import zio.IO -import zio.test.Assertion._ import zio.test._ object RenderingSpec extends ZIOSpecDefault { @@ -174,49 +173,49 @@ object RenderingSpec extends ZIOSpecDefault { rendered == """schema{query:Query} "Description of custom scalar emphasizing proper captain ship names" scalar CaptainShipName @specifiedBy(url:"http://someUrl") @tag union Role @uniondirective=Captain|Engineer|Mechanic|Pilot enum Origin @enumdirective{BELT,EARTH,MARS,MOON @deprecated(reason:"Use: EARTH | MARS | BELT")} input CharacterInput @inputobjdirective{name:String! @external nicknames:[String!]! @required origin:Origin!}interface Human{ name:String! @external}type Captain{ shipName:CaptainShipName!}type Character implements Human @key(name:"name"){ name:String! @external nicknames:[String!]! @required origin:Origin! role:Role}type Engineer{ shipName:String!}type Mechanic{ shipName:String!}type Narrator implements Human{ name:String!}type Pilot{ shipName:String!}"Queries" type Query{ "Return all characters from a given origin" characters(origin:Origin):[Character!]! character(name:String!):Character @deprecated(reason:"Use `characters`") charactersIn(names:[String!]! @lowercase):[Character!]! exists(character:CharacterInput!):Boolean! human:Human!}""" ) }, - test("@oneOf input as value type") { - case class Queries(foo: Foo => String) - - implicit val schema: Schema[Any, Queries] = Schema.gen - val resolver = RootResolver(Queries(_.toString)) - - val expected = - """schema { - | query: Queries - |} - | - |input FooInput @oneOf { - | intValue: Int - | stringValue: String - |} - | - |type Queries { - | foo(value: FooInput!): String! - |}""".stripMargin - - assertTrue(graphQL(resolver).render == expected) - }, - test("@oneOf input wrapped in a case class") { - case class Queries(foo: Foo.Wrapped => String) - - implicit val schema: Schema[Any, Queries] = Schema.gen - val resolver = RootResolver(Queries(_.toString)) - - val expected = - """schema { - | query: Queries - |} - | - |input FooInput @oneOf { - | intValue: Int - | stringValue: String - |} - | - |type Queries { - | foo(fooInput: FooInput!): String! - |}""".stripMargin - - assertTrue(graphQL(resolver).render == expected) + suite("@oneOf inputs") { + def expected(label: String) = + s"""schema { + | query: Queries + |} + | + |input FooInput @oneOf { + | intValue: FooIntInput + | stringValue: String + | otherIntField: OtherIntFieldInput + | otherStringField: String + |} + | + |input FooIntInput { + | intValue: Int! + |} + | + |input OtherIntFieldInput { + | intValue: Int! + |} + | + |type Queries { + | foo($label: FooInput!): String! + |}""".stripMargin + + List( + test("as value types") { + case class Queries(foo: Foo => String) + + implicit val schema: Schema[Any, Queries] = Schema.gen + val resolver = RootResolver(Queries(_.toString)) + + assertTrue(graphQL(resolver).render == expected("value")) + }, + test("wrapped in a case class") { + case class Queries(foo: Foo.Wrapped => String) + + implicit val schema: Schema[Any, Queries] = Schema.gen + val resolver = RootResolver(Queries(_.toString)) + + assertTrue(graphQL(resolver).render == expected("fooInput")) + } + ) } ) @@ -224,8 +223,14 @@ object RenderingSpec extends ZIOSpecDefault { sealed trait Foo object Foo { + @GQLValueType + @GQLOneOfInputName("stringValue") case class FooString(stringValue: String) extends Foo - case class FooInt(intValue: Int) extends Foo + @GQLValueType + case class OtherStringField(someField: String) extends Foo + @GQLOneOfInputName("intValue") + case class FooInt(intValue: Int) extends Foo + case class OtherIntField(intValue: Int) extends Foo case class Wrapped(fooInput: Foo) } diff --git a/core/src/test/scala/caliban/TestUtils.scala b/core/src/test/scala/caliban/TestUtils.scala index 7530a19bd2..d467c94acb 100644 --- a/core/src/test/scala/caliban/TestUtils.scala +++ b/core/src/test/scala/caliban/TestUtils.scala @@ -106,7 +106,11 @@ object TestUtils { @GQLOneOfInput sealed trait NameOrOrigin object NameOrOrigin { - case class ByName(name: String) extends NameOrOrigin + @GQLValueType + @GQLOneOfInputName("name") + case class ByName(name: String) extends NameOrOrigin + @GQLValueType + @GQLOneOfInputName("origin") case class ByOrigin(origin: Origin) extends NameOrOrigin case class Wrapper(nameOrOrigin: NameOrOrigin) @@ -141,7 +145,7 @@ object TestUtils { implicit val characterSchema: Schema[Any, Character] = genAll implicit val querySchema: Schema[Any, Query] = genAll implicit val queryIOSchema: Schema[Any, QueryIO] = genAll - implicit val queryOneOffSchema: Schema[Any, QueryWithOneOf] = genAll + implicit val queryOneOfSchema: Schema[Any, QueryWithOneOf] = genAll implicit val mutationIOSchema: Schema[Any, MutationIO] = genAll implicit val subscriptionIOSchema: Schema[Any, SubscriptionIO] = genAll diff --git a/core/src/test/scala/caliban/execution/ExecutionSpec.scala b/core/src/test/scala/caliban/execution/ExecutionSpec.scala index 39ba6a2876..6db4775e4d 100644 --- a/core/src/test/scala/caliban/execution/ExecutionSpec.scala +++ b/core/src/test/scala/caliban/execution/ExecutionSpec.scala @@ -4,15 +4,15 @@ import java.util.UUID import caliban.CalibanError.ExecutionError import caliban.Macros.gqldoc import caliban.TestUtils._ -import caliban.Value.{ BooleanValue, IntValue, NullValue, StringValue } +import caliban.Value.{BooleanValue, IntValue, NullValue, StringValue} import caliban.introspection.adt.__Type import caliban.parsing.adt.LocationInfo -import caliban.schema.Annotations.{ GQLInterface, GQLName, GQLOneOfInput, GQLValueType } +import caliban.schema.Annotations.{GQLInterface, GQLName, GQLOneOfInput, GQLOneOfInputName, GQLValueType} import caliban.schema._ import caliban.schema.Schema.auto._ import caliban.schema.ArgBuilder.auto._ import caliban._ -import zio.{ FiberRef, IO, Task, UIO, ZIO, ZLayer } +import zio.{FiberRef, IO, Task, UIO, ZIO, ZLayer} import zio.stream.ZStream import zio.test._ @@ -1253,7 +1253,11 @@ object ExecutionSpec extends ZIOSpecDefault { @GQLOneOfInput sealed trait Foo object Foo { + @GQLValueType + @GQLOneOfInputName("stringValue") case class FooString(stringValue: String) extends Foo + @GQLValueType + @GQLOneOfInputName("intValue") case class FooInt(intValue: Int) extends Foo case class Wrapper(fooInput: Foo) diff --git a/core/src/test/scala/caliban/introspection/IntrospectionSpec.scala b/core/src/test/scala/caliban/introspection/IntrospectionSpec.scala index c97ac0ca58..d57c47fdc3 100644 --- a/core/src/test/scala/caliban/introspection/IntrospectionSpec.scala +++ b/core/src/test/scala/caliban/introspection/IntrospectionSpec.scala @@ -158,7 +158,7 @@ object IntrospectionSpec extends ZIOSpecDefault { ) } }, - test("introspect schema with oneOff") { + test("introspect schema with oneOf") { val interpreter = graphQL(resolverOneOf).interpreter interpreter.flatMap(_.execute(fullIntrospectionQuery)).map { response => @@ -196,7 +196,7 @@ object IntrospectionSpec extends ZIOSpecDefault { assertTrue(response.data.toString == """{"__type":null}""") } }, - test("introspect oneOff types") { + test("introspect oneOf types") { val interpreter = graphQL(resolverOneOf).interpreter def query(name: String) = diff --git a/core/src/test/scala/caliban/schema/ArgBuilderSpec.scala b/core/src/test/scala/caliban/schema/ArgBuilderSpec.scala index 6be97248b0..882daa9a18 100644 --- a/core/src/test/scala/caliban/schema/ArgBuilderSpec.scala +++ b/core/src/test/scala/caliban/schema/ArgBuilderSpec.scala @@ -5,7 +5,7 @@ import caliban.InputValue import caliban.InputValue.ObjectValue import caliban.schema.ArgBuilder.auto._ import caliban.Value.{ IntValue, NullValue, StringValue } -import caliban.schema.Annotations.GQLOneOfInput +import caliban.schema.Annotations.{ GQLOneOfInput, GQLOneOfInputName, GQLValueType } import zio.test.Assertion._ import zio.test._ @@ -99,31 +99,49 @@ object ArgBuilderSpec extends ZIOSpecDefault { } ), suite("oneOf") { - @GQLOneOfInput sealed trait Foo + object Foo { + @GQLValueType + @GQLOneOfInputName("stringValue") case class FooString(stringValue: String) extends Foo - case class FooInt(intValue: Int) extends Foo + + @GQLValueType + case class FooString2(someField: String) extends Foo + + @GQLOneOfInputName("intValue") + case class FooInt(foo1: Int) extends Foo + + case class FooInt2(foo2: Int) extends Foo } - implicit val fooStringAb: ArgBuilder[Foo.FooString] = ArgBuilder.gen - implicit val fooIntAb: ArgBuilder[Foo.FooInt] = ArgBuilder.gen - val fooAb: ArgBuilder[Foo] = ArgBuilder.gen + implicit val f1Ab: ArgBuilder[Foo.FooString] = ArgBuilder.gen + implicit val f2Ab: ArgBuilder[Foo.FooString2] = ArgBuilder.gen + implicit val f3Ab: ArgBuilder[Foo.FooInt] = ArgBuilder.gen + implicit val f4Ab: ArgBuilder[Foo.FooInt2] = ArgBuilder.gen + val fooAb: ArgBuilder[Foo] = ArgBuilder.gen List( test("valid input") { - assertTrue( - fooAb.build(ObjectValue(Map("stringValue" -> StringValue("foo")))) == Right(Foo.FooString("foo")), - fooAb.build(ObjectValue(Map("intValue" -> IntValue(42)))) == Right(Foo.FooInt(42)) + val inputs = List( + Map("stringValue" -> StringValue("foo")) -> Foo.FooString("foo"), + Map("fooString2" -> StringValue("foo2")) -> Foo.FooString2("foo2"), + Map("intValue" -> ObjectValue(Map("foo1" -> IntValue(42)))) -> Foo.FooInt(42), + Map("fooInt2" -> ObjectValue(Map("foo2" -> IntValue(42)))) -> Foo.FooInt2(42) ) + + inputs.foldLeft(assertCompletes) { case (acc, (input, expected)) => + acc && assertTrue(fooAb.build(ObjectValue(input)) == Right(expected)) + } }, test("invalid input") { List( Map("invalid" -> StringValue("foo")), - Map("stringValue" -> StringValue("foo"), "intValue" -> IntValue(42)), + Map("stringValue" -> StringValue("foo"), "fooString2" -> StringValue("foo2")), + Map("intValue" -> IntValue(42)), Map("stringValue" -> NullValue), - Map("stringValue" -> NullValue, "invalid" -> NullValue) + Map("stringValue" -> StringValue("foo"), "invalid" -> NullValue) ) .map(input => assertTrue(fooAb.build(ObjectValue(input)).isLeft)) .foldLeft(assertCompletes)(_ && _) From e0c0e90548f2b0de1bdc9021d694d66c2fed87d8 Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Sun, 27 Aug 2023 11:09:32 +0300 Subject: [PATCH 10/25] fmt --- .../scala-3/caliban/schema/ArgBuilderDerivation.scala | 2 +- core/src/test/scala/caliban/execution/ExecutionSpec.scala | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala b/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala index 32e23727e5..6ffaa64fda 100644 --- a/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala +++ b/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala @@ -100,7 +100,7 @@ trait CommonArgBuilderDerivation { .toRight(ExecutionError(s"Invalid oneOf input $fields for trait $traitLabel")) .flatMap { case (builder, true) => builder.build(innerValue) - case (builder, _) => + case (builder, _) => innerValue match { case _: InputValue.ObjectValue => builder.build(innerValue) case _ => Left(ExecutionError(s"Can't build a trait from input $input")) diff --git a/core/src/test/scala/caliban/execution/ExecutionSpec.scala b/core/src/test/scala/caliban/execution/ExecutionSpec.scala index 6db4775e4d..aa241fe455 100644 --- a/core/src/test/scala/caliban/execution/ExecutionSpec.scala +++ b/core/src/test/scala/caliban/execution/ExecutionSpec.scala @@ -4,15 +4,15 @@ import java.util.UUID import caliban.CalibanError.ExecutionError import caliban.Macros.gqldoc import caliban.TestUtils._ -import caliban.Value.{BooleanValue, IntValue, NullValue, StringValue} +import caliban.Value.{ BooleanValue, IntValue, NullValue, StringValue } import caliban.introspection.adt.__Type import caliban.parsing.adt.LocationInfo -import caliban.schema.Annotations.{GQLInterface, GQLName, GQLOneOfInput, GQLOneOfInputName, GQLValueType} +import caliban.schema.Annotations.{ GQLInterface, GQLName, GQLOneOfInput, GQLOneOfInputName, GQLValueType } import caliban.schema._ import caliban.schema.Schema.auto._ import caliban.schema.ArgBuilder.auto._ import caliban._ -import zio.{FiberRef, IO, Task, UIO, ZIO, ZLayer} +import zio.{ FiberRef, IO, Task, UIO, ZIO, ZLayer } import zio.stream.ZStream import zio.test._ @@ -1258,7 +1258,7 @@ object ExecutionSpec extends ZIOSpecDefault { case class FooString(stringValue: String) extends Foo @GQLValueType @GQLOneOfInputName("intValue") - case class FooInt(intValue: Int) extends Foo + case class FooInt(intValue: Int) extends Foo case class Wrapper(fooInput: Foo) } From 648680cf4aadda927bc57ad83676daea469cb0c5 Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Sun, 27 Aug 2023 17:46:55 +0300 Subject: [PATCH 11/25] Add schema & input validations --- .../caliban/schema/ArgBuilderDerivation.scala | 40 +++--- .../caliban/schema/ArgBuilderDerivation.scala | 41 +++--- .../caliban/introspection/adt/__Type.scala | 2 +- .../scala/caliban/validation/Validator.scala | 64 +++++++-- .../caliban/execution/ExecutionSpec.scala | 25 ++-- .../caliban/validation/ValidationSpec.scala | 132 +++++++++++++++++- 6 files changed, 238 insertions(+), 66 deletions(-) diff --git a/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala b/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala index 8f5f766497..82ede40c75 100644 --- a/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala @@ -61,31 +61,31 @@ trait CommonArgBuilderDerivation { private def makeOneOfBuilder[A](ctx: SealedTrait[ArgBuilder, A]): ArgBuilder[A] = new ArgBuilder[A] { - private val builders = ctx.subtypes.map { p => - val name = Types.oneOfInputFieldName(p.typeName.short, p.annotations) - val isValueType = p.annotations.exists { case GQLValueType(_) => true; case _ => false } + private val builders = + ctx.subtypes.map { p => + val name = Types.oneOfInputFieldName(p.typeName.short, p.annotations) + val isValueType = p.annotations.exists { case GQLValueType(_) => true; case _ => false } + name -> (p.typeclass, isValueType) + }.toMap - name -> (p.typeclass, isValueType) - }.toMap + private def error(input: InputValue) = + ExecutionError(s"Invalid oneOf input $input for trait ${ctx.typeName.short}") def build(input: InputValue): Either[ExecutionError, A] = input match { - case InputValue.ObjectValue(fields) if fields.size == 1 => - val (key, innerValue) = fields.head - builders - .get(key) - .toRight(ExecutionError(s"Invalid oneOf input $fields for trait ${ctx.typeName.short}}")) - .flatMap { - case (builder, true) => builder.build(innerValue) - case (builder, _) => + case InputValue.ObjectValue(fields) => + fields.toList match { + case (key, innerValue) :: Nil => + builders.get(key).toRight(error(input)).flatMap { case (builder, isValueType) => innerValue match { - case _: InputValue.ObjectValue => builder.build(innerValue) - case _ => Left(ExecutionError(s"Can't build a trait from input $input")) + case v: InputValue.ObjectValue if !isValueType => builder.build(v) + case v if isValueType => builder.build(v) + case _ => Left(error(input)) } - } - case InputValue.ObjectValue(_) => - Left(ExecutionError("Exactly one key must be specified for oneOf inputs")) - case _ => - Left(ExecutionError(s"Can't build a trait from input $input")) + } + case _ => + Left(ExecutionError("Exactly one key must be specified for oneOf inputs")) + } + case _ => Left(error(input)) } } diff --git a/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala b/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala index 6ffaa64fda..47b3753bff 100644 --- a/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala +++ b/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala @@ -83,33 +83,32 @@ trait CommonArgBuilderDerivation { _traitLabel: => String ): ArgBuilder[A] = new ArgBuilder[A] { - private val builders = _subTypes.map { (label, annotations, argBuilder) => - val name = Types.oneOfInputFieldName(label, annotations) - val isValueType = annotations.exists { case GQLValueType(_) => true; case _ => false } - - name -> (argBuilder, isValueType) - }.toMap.asInstanceOf[Map[String, (ArgBuilder[A], Boolean)]] + private lazy val builders: Map[String, (ArgBuilder[A], Boolean)] = + _subTypes.map { (label, annotations, argBuilder) => + val name = Types.oneOfInputFieldName(label, annotations) + val isValueType = annotations.exists { case GQLValueType(_) => true; case _ => false } + name -> (argBuilder.asInstanceOf[ArgBuilder[A]], isValueType) + }.toMap private lazy val traitLabel = _traitLabel + private def error(input: InputValue) = ExecutionError(s"Invalid oneOf input $input for trait $traitLabel") + def build(input: InputValue): Either[ExecutionError, A] = input match { - case InputValue.ObjectValue(fields) if fields.size == 1 => - val (key, innerValue) = fields.head - builders - .get(key) - .toRight(ExecutionError(s"Invalid oneOf input $fields for trait $traitLabel")) - .flatMap { - case (builder, true) => builder.build(innerValue) - case (builder, _) => + case InputValue.ObjectValue(fields) => + fields.toList match { + case (key, innerValue) :: Nil => + builders.get(key).toRight(error(input)).flatMap { (builder, isValueType) => innerValue match { - case _: InputValue.ObjectValue => builder.build(innerValue) - case _ => Left(ExecutionError(s"Can't build a trait from input $input")) + case v: InputValue.ObjectValue if !isValueType => builder.build(v) + case v if isValueType => builder.build(v) + case _ => Left(error(input)) } - } - case InputValue.ObjectValue(_) => - Left(ExecutionError("Exactly one key must be specified for oneOf inputs")) - case _ => - Left(ExecutionError(s"Can't build a trait from input $input")) + } + case _ => + Left(ExecutionError("Exactly one key must be specified for oneOf inputs")) + } + case _ => Left(error(input)) } } diff --git a/core/src/main/scala/caliban/introspection/adt/__Type.scala b/core/src/main/scala/caliban/introspection/adt/__Type.scala index 36fce00e7c..43cf22fd58 100644 --- a/core/src/main/scala/caliban/introspection/adt/__Type.scala +++ b/core/src/main/scala/caliban/introspection/adt/__Type.scala @@ -131,5 +131,5 @@ case class __Type( lazy val innerType: __Type = Types.innerType(this) - def _isOneOfInput: Boolean = isOneOf.getOrElse(false) + private[caliban] def _isOneOfInput: Boolean = isOneOf.getOrElse(false) } diff --git a/core/src/main/scala/caliban/validation/Validator.scala b/core/src/main/scala/caliban/validation/Validator.scala index fe2f3c8410..69195e29a2 100644 --- a/core/src/main/scala/caliban/validation/Validator.scala +++ b/core/src/main/scala/caliban/validation/Validator.scala @@ -82,6 +82,11 @@ object Validator { def failValidation(msg: String, explanatoryText: String): EReader[Any, ValidationError, Nothing] = ZPure.fail(ValidationError(msg, explanatoryText)) + private def failValidationWhen( + cond: Boolean + )(msg: => String, explanatoryText: => String): EReader[Any, ValidationError, Unit] = + if (cond) failValidation(msg, explanatoryText) else ZPure.unit + /** * Prepare the request for execution. * Fails with a [[caliban.CalibanError.ValidationError]] otherwise. @@ -353,14 +358,20 @@ object Validator { ) ) } *> ZPure.foreachDiscard(op.variableDefinitions) { v => - val t = Type.innerType(v.variableType) - ZPure.whenCase(context.rootType.types.get(t).map(_.kind)) { + val t = context.rootType.types.get(Type.innerType(v.variableType)) + ZPure.whenCase(t.map(_.kind)) { case Some(__TypeKind.OBJECT) | Some(__TypeKind.UNION) | Some(__TypeKind.INTERFACE) => failValidation( s"Type of variable '${v.name}' is not a valid input type.", "Variables can only be input types. Objects, unions, and interfaces cannot be used as inputs." ) - } + } *> + ZPure.when(t.flatMap(_.isOneOf).getOrElse(false)) { + validateOneOfInputValue( + context.variables.getOrElse(v.name, NullValue), + s"Variable '${v.name}'" + ) + } } *> { val variableUsages = collectVariablesUsed(context, op.selectionSet) ZPure.foreachDiscard(variableUsages)(v => @@ -525,8 +536,14 @@ object Validator { currentType: __Type, context: Context ): EReader[Any, ValidationError, Unit] = - ZPure.foreachDiscard(f.args.filter(_._type.kind == __TypeKind.NON_NULL))(arg => - (arg.defaultValue, field.arguments.get(arg.name)) match { + ZPure.foreachDiscard(f.args.filter(_._type.kind == __TypeKind.NON_NULL)) { arg => + val fieldArgs = field.arguments.get(arg.name) + ZPure.when(arg._type.innerType._isOneOfInput) { + validateOneOfInputValue( + fieldArgs.getOrElse(NullValue), + s"Argument '${arg.name}' on field '${field.name}'" + ) + } *> ZPure.whenCase(arg.defaultValue, fieldArgs) { case (None, None) | (None, Some(NullValue)) => failValidation( s"Required argument '${arg.name}' is null or missing on field '${field.name}' of type '${currentType.name @@ -540,9 +557,8 @@ object Validator { .getOrElse("")}'.", "Arguments can be required. An argument is required if the argument type is non‐null and does not have a default value. Otherwise, the argument is optional." ) - case _ => ZPure.unit[Unit] } - ) *> + } *> ZPure.foreachDiscard(field.arguments) { case (arg, argValue) => f.args.find(_.name == arg) match { case None => @@ -612,6 +628,23 @@ object Validator { } } *> ValueValidator.validateInputTypes(inputValue, argValue, context, errorContext) + private def validateOneOfInputValue( + inputValue: InputValue, + errorContext: => String + ): EReader[Any, ValidationError, Unit] = + ZPure + .whenCase(inputValue match { + case InputValue.ObjectValue(fields) if fields.size == 1 => fields.headOption.map(_._2) + case vv: InputValue.VariableValue => Some(vv) + case _ => None + }) { case None | Some(NullValue) => + failValidation( + s"$errorContext is not a valid @oneOf input", + "@oneOf input arguments must specify exactly one non-null key" + ) + } + .unit + private def checkVariableUsageAllowed( variableDefinition: VariableDefinition, inputValue: __InputValue @@ -837,13 +870,28 @@ object Validator { ZPure.foreachDiscard(fields)(validateInputValue(_, inputObjectContext)) *> noDuplicateInputValueName(fields, inputObjectContext) + def validateOneOfFields(fields: List[__InputValue]): EReader[Any, ValidationError, Unit] = + failValidationWhen(fields.size <= 1)( + s"$inputObjectContext @oneOf has less than 2 fields", + "@oneOf inputs must have at least 2 possible fields" + ) *> ZPure.foreachDiscard(fields) { f => + failValidationWhen(f.defaultValue.isDefined)( + s"$inputObjectContext @oneOf argument has a default value", + "@oneOf input fields cannot have default values" + ) *> + failValidationWhen(!f._type.isNullable)( + s"$inputObjectContext @oneOf argument is not nullable", + "All of @oneOf input fields must be nullable according to the spec" + ) + } + t.inputFields match { case None | Some(Nil) => failValidation( s"$inputObjectContext does not have fields", "An Input Object type must define one or more input fields" ) - case Some(fields) => validateFields(fields) + case Some(fields) => ZPure.when(t._isOneOfInput)(validateOneOfFields(fields)) *> validateFields(fields) } } diff --git a/core/src/test/scala/caliban/execution/ExecutionSpec.scala b/core/src/test/scala/caliban/execution/ExecutionSpec.scala index aa241fe455..a79665b6ee 100644 --- a/core/src/test/scala/caliban/execution/ExecutionSpec.scala +++ b/core/src/test/scala/caliban/execution/ExecutionSpec.scala @@ -2,6 +2,7 @@ package caliban.execution import java.util.UUID import caliban.CalibanError.ExecutionError +import caliban.InputValue.ObjectValue import caliban.Macros.gqldoc import caliban.TestUtils._ import caliban.Value.{ BooleanValue, IntValue, NullValue, StringValue } @@ -1285,22 +1286,18 @@ object ExecutionSpec extends ZIOSpecDefault { ) ) - val q1 = gqldoc("""{ foo(fooInput: {stringValue: "hello"}) }""") - val q2 = gqldoc("""{ foo(fooInput: {intValue: 42}) }""") - val q3 = gqldoc("""{ fooUnwrapped(value: {intValue: 42}) }""") - - for { - _ <- api.validateRootSchema - i <- api.interpreter - r1 <- i.execute(q1) - r2 <- i.execute(q2) - r3 <- i.execute(q3) - } yield assertTrue( - r1.data.toString == """{"foo":"hello"}""", - r2.data.toString == """{"foo":"42"}""", - r3.data.toString == """{"fooUnwrapped":"42"}""" + val cases = List( + gqldoc("""{ foo(fooInput: {stringValue: "hello"}) }""") -> """{"foo":"hello"}""", + gqldoc("""{ foo(fooInput: {intValue: 42}) }""") -> """{"foo":"42"}""", + gqldoc("""{ fooUnwrapped(value: {intValue: 42}) }""") -> """{"fooUnwrapped":"42"}""", + gqldoc("""query Foo($args: FooInput!){ fooUnwrapped(value: $args) }""") -> """{"fooUnwrapped":"42"}""" ) + 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)) + } } ) } diff --git a/core/src/test/scala/caliban/validation/ValidationSpec.scala b/core/src/test/scala/caliban/validation/ValidationSpec.scala index 81e2f4eb0d..bcfc888ba2 100644 --- a/core/src/test/scala/caliban/validation/ValidationSpec.scala +++ b/core/src/test/scala/caliban/validation/ValidationSpec.scala @@ -2,10 +2,12 @@ package caliban.validation import caliban._ import caliban.CalibanError.ValidationError +import caliban.InputValue.ObjectValue import caliban.Macros.gqldoc import caliban.TestUtils._ -import caliban.Value.{ BooleanValue, IntValue, StringValue } -import caliban.schema.Annotations.GQLDefault +import caliban.Value.{ BooleanValue, IntValue, NullValue, StringValue } +import caliban.schema.Annotations.{ GQLDefault, GQLOneOfInput, GQLOneOfInputName, GQLValueType } +import caliban.schema.{ ArgBuilder, Schema } import zio.{ IO, UIO, ZIO } import zio.test.Assertion._ import zio.test._ @@ -383,6 +385,132 @@ object ValidationSpec extends ZIOSpecDefault { api.interpreter.flatMap(_.execute(query)).map { response => assertTrue(response.errors.isEmpty) } + }, + suite("@oneOf inputs") { + import caliban.schema.Schema.auto._ + import caliban.schema.ArgBuilder.auto._ + + @GQLOneOfInput + sealed trait Foo + object Foo { + @GQLValueType + case class FooString(stringValue: String) extends Foo + @GQLOneOfInputName("fooInt") + case class IntObj(intValue: Int) extends Foo + case class Wrapper(fooInput: Foo) + } + + case class Bar(foo2: Foo => String) + case class Queries(foo: Foo.Wrapper => String, fooUnwrapped: Foo => String, bar: Bar) + + implicit val fooStringAb: ArgBuilder[Foo.FooString] = ArgBuilder.gen + implicit val fooIntAb: ArgBuilder[Foo.IntObj] = ArgBuilder.gen + implicit val fooAb: ArgBuilder[Foo] = ArgBuilder.gen + implicit val barSchema: Schema[Any, Bar] = Schema.gen + implicit val schema: Schema[Any, Queries] = Schema.gen + + val api: GraphQL[Any] = graphQL(RootResolver(Queries(_.fooInput.toString, _.toString, Bar(_.toString)))) + + def argumentsQuery(arg1: ObjectValue, arg2: ObjectValue, arg3: ObjectValue) = + s""" + |{ + | foo(fooInput: ${arg1.toInputString}) + | + | fooUnwrapped(value: ${arg2.toInputString}) + | + | bar { + | foo2(value: ${arg3.toInputString} ) + | } + |} + |""".stripMargin + + val variablesQuery = + """ + |query Foo($args1: FooInput!, $args2: FooInput!, $args3: FooInput!){ + | foo(fooInput: $args1) + | + | fooUnwrapped(value: $args2) + | + | bar { + | foo2(value: $args3 ) + | } + |} + |""".stripMargin + + val validInputs = List( + Map("fooString" -> StringValue("hello")), + Map("fooInt" -> ObjectValue(Map("intValue" -> IntValue(42)))) + ).map(ObjectValue(_)) + + val invalidInputs = List( + Map.empty[String, InputValue], + Map("fooString" -> NullValue), + Map("fooString" -> StringValue("foo"), "fooInt" -> NullValue), + Map("fooString" -> StringValue("foo"), "fooInt" -> ObjectValue(Map("intValue" -> IntValue(42)))) + ).map(ObjectValue(_)) + + val validVariablesCases = validInputs.map(arg => Map("args1" -> arg, "args2" -> arg, "args3" -> arg)) + + List( + test("valid field arguments") { + val cases = validInputs.map(v => argumentsQuery(v, v, v)) + ZIO.foldLeft(cases)(assertCompletes) { case (acc, query) => + api.interpreter + .flatMap(_.execute(query)) + .map(resp => acc && assertTrue(resp.errors.isEmpty)) + } + }, + test("valid variables") { + ZIO.foldLeft(validVariablesCases)(assertCompletes) { case (acc, variables) => + api.interpreter + .flatMap(_.execute(variablesQuery, variables = variables)) + .map(resp => acc && assertTrue(resp.errors.isEmpty)) + } + }, + test("invalid field arguments") { + val cases = for { + invalid <- invalidInputs + valid = validInputs.head + _case <- List( + argumentsQuery(invalid, valid, valid), + argumentsQuery(valid, invalid, valid), + argumentsQuery(valid, valid, invalid) + ) + } yield _case + + ZIO.foldLeft(cases)(assertCompletes) { case (acc, query) => + api.interpreter + .flatMap(_.execute(query)) + .map(resp => + acc && assertTrue( + resp.errors.nonEmpty && resp.errors.forall { + case ValidationError(msg, _, _, _) => msg.contains("is not a valid @oneOf input") + case _ => false + } + ) + ) + } + }, + test("invalid variables") { + val cases = for { + argName <- validVariablesCases.head.keys + invalid <- invalidInputs + } yield validVariablesCases.head.updated(argName, invalid) + + ZIO.foldLeft(cases)(assertCompletes) { case (acc, variables) => + api.interpreter + .flatMap(_.execute(variablesQuery, variables = variables)) + .map(resp => + acc && assertTrue( + resp.errors.nonEmpty && resp.errors.forall { + case ValidationError(msg, _, _, _) => msg.contains("is not a valid @oneOf input") + case _ => false + } + ) + ) + } + } + ) } ) } From 047c32405c66d7969e7396eefacefe01fbca9191 Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Sun, 27 Aug 2023 17:59:20 +0300 Subject: [PATCH 12/25] Remove `nullable` methods and add schema validation tests --- .../introspection/adt/__InputValue.scala | 2 -- .../caliban/introspection/adt/__Type.scala | 9 ++------- .../caliban/validation/ValidationSpec.scala | 18 ++++++++++++++++++ 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/core/src/main/scala/caliban/introspection/adt/__InputValue.scala b/core/src/main/scala/caliban/introspection/adt/__InputValue.scala index b699b411fe..44fe72fcbf 100644 --- a/core/src/main/scala/caliban/introspection/adt/__InputValue.scala +++ b/core/src/main/scala/caliban/introspection/adt/__InputValue.scala @@ -17,6 +17,4 @@ case class __InputValue( } private[caliban] lazy val _type: __Type = `type`() - - private[caliban] def nullable: __InputValue = copy(`type` = () => _type.nullable) } diff --git a/core/src/main/scala/caliban/introspection/adt/__Type.scala b/core/src/main/scala/caliban/introspection/adt/__Type.scala index 43cf22fd58..cde8836e92 100644 --- a/core/src/main/scala/caliban/introspection/adt/__Type.scala +++ b/core/src/main/scala/caliban/introspection/adt/__Type.scala @@ -118,13 +118,8 @@ case class __Type( case _ => true } - def list: __Type = __Type(__TypeKind.LIST, ofType = Some(self)) - def nonNull: __Type = __Type(__TypeKind.NON_NULL, ofType = Some(self)) - def nullable: __Type = - (kind, ofType) match { - case (__TypeKind.NON_NULL, Some(inner)) => inner - case _ => self - } + def list: __Type = __Type(__TypeKind.LIST, ofType = Some(self)) + def nonNull: __Type = __Type(__TypeKind.NON_NULL, ofType = Some(self)) lazy val allFields: List[__Field] = fields(__DeprecatedArgs(Some(true))).getOrElse(Nil) diff --git a/core/src/test/scala/caliban/validation/ValidationSpec.scala b/core/src/test/scala/caliban/validation/ValidationSpec.scala index bcfc888ba2..b647bf45a0 100644 --- a/core/src/test/scala/caliban/validation/ValidationSpec.scala +++ b/core/src/test/scala/caliban/validation/ValidationSpec.scala @@ -509,6 +509,24 @@ object ValidationSpec extends ZIOSpecDefault { ) ) } + }, + test("schema is valid") { + api.validateRootSchema.as(assertCompletes) + }, + test("schema is invalid") { + @GQLOneOfInput + sealed trait Foo + object Foo { + case class Bar(value: String) extends Foo + } + + case class Queries(foo: Foo => String) + implicit val abInner: ArgBuilder[Foo.Bar] = ArgBuilder.gen + implicit val ab: ArgBuilder[Foo] = ArgBuilder.gen + implicit val schema: Schema[Any, Queries] = Schema.gen + + graphQL(RootResolver(Queries(_.toString))).validateRootSchema + .fold(_ => assertCompletes, _ => assertNever("Schema should be invalid")) } ) } From 6f9a48153c588d3dd200ad3bf3abea5a7f5f1c5e Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Tue, 29 Aug 2023 00:46:32 +0300 Subject: [PATCH 13/25] PR comments --- .../caliban/schema/ArgBuilderDerivation.scala | 35 ++++------ .../caliban/schema/SchemaDerivation.scala | 14 ++-- .../caliban/schema/ArgBuilderDerivation.scala | 57 +++++++--------- .../caliban/schema/SchemaDerivation.scala | 13 ++-- .../introspection/adt/__InputValue.scala | 21 +++++- .../scala/caliban/schema/Annotations.scala | 4 -- .../src/main/scala/caliban/schema/Types.scala | 7 -- .../scala/caliban/validation/Validator.scala | 61 ++++++++++------- .../test/scala/caliban/RenderingSpec.scala | 35 +++++----- core/src/test/scala/caliban/TestUtils.scala | 6 +- .../caliban/execution/ExecutionSpec.scala | 8 +-- .../introspection/IntrospectionSpec.scala | 4 +- .../scala/caliban/schema/ArgBuilderSpec.scala | 41 ++++++------ .../validation/ValidationSchemaSpec.scala | 66 ++++++++++++++++++- .../caliban/validation/ValidationSpec.scala | 53 ++++++++------- 15 files changed, 239 insertions(+), 186 deletions(-) diff --git a/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala b/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala index 82ede40c75..f5da89ca57 100644 --- a/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala @@ -3,7 +3,7 @@ package caliban.schema import caliban.CalibanError.ExecutionError import caliban.InputValue import caliban.Value._ -import caliban.schema.Annotations.{ GQLDefault, GQLName, GQLOneOfInput, GQLOneOfInputName, GQLValueType } +import caliban.schema.Annotations.{ GQLDefault, GQLName, GQLOneOfInput } import magnolia._ import mercator.Monadic @@ -61,31 +61,20 @@ trait CommonArgBuilderDerivation { private def makeOneOfBuilder[A](ctx: SealedTrait[ArgBuilder, A]): ArgBuilder[A] = new ArgBuilder[A] { - private val builders = - ctx.subtypes.map { p => - val name = Types.oneOfInputFieldName(p.typeName.short, p.annotations) - val isValueType = p.annotations.exists { case GQLValueType(_) => true; case _ => false } - name -> (p.typeclass, isValueType) - }.toMap - - private def error(input: InputValue) = + private def inputError(input: InputValue) = ExecutionError(s"Invalid oneOf input $input for trait ${ctx.typeName.short}") + private val combined = ctx.subtypes.map(_.typeclass).toList.asInstanceOf[List[ArgBuilder[A]]] match { + case head :: tail => + tail.foldLeft(head)(_ orElse _).orElse(input => Left(inputError(input))) + case _ => + (_ => Left(ExecutionError("OneOf Input Objects must have at least one subtype"))): ArgBuilder[A] + } + def build(input: InputValue): Either[ExecutionError, A] = input match { - case InputValue.ObjectValue(fields) => - fields.toList match { - case (key, innerValue) :: Nil => - builders.get(key).toRight(error(input)).flatMap { case (builder, isValueType) => - innerValue match { - case v: InputValue.ObjectValue if !isValueType => builder.build(v) - case v if isValueType => builder.build(v) - case _ => Left(error(input)) - } - } - case _ => - Left(ExecutionError("Exactly one key must be specified for oneOf inputs")) - } - case _ => Left(error(input)) + case InputValue.ObjectValue(f) if f.size == 1 => combined.build(input) + case InputValue.ObjectValue(_) => Left(ExecutionError("Exactly one key must be specified for oneOf inputs")) + case _ => Left(inputError(input)) } } diff --git a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala index 20fc290043..911558b846 100644 --- a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala @@ -1,5 +1,6 @@ package caliban.schema +import caliban.CalibanError.ValidationError import caliban.Value._ import caliban.introspection.adt._ import caliban.parsing.adt.Directive @@ -54,7 +55,8 @@ trait CommonSchemaDerivation[R] { if (p.typeclass.optional) p.typeclass.toType_(isInput, isSubscription) else p.typeclass.toType_(isInput, isSubscription).nonNull, p.annotations.collectFirst { case GQLDefault(v) => v }, - Some(p.annotations.collect { case GQLDirective(dir) => dir }.toList).filter(_.nonEmpty) + Some(p.annotations.collect { case GQLDirective(dir) => dir }.toList).filter(_.nonEmpty), + Some(ctx.typeName.short), ) ) .toList, @@ -153,14 +155,8 @@ trait CommonSchemaDerivation[R] { Some(ctx.annotations.collectFirst { case GQLInputName(suffix) => suffix } .getOrElse(customizeInputTypeName(getName(ctx)))), getDescription(ctx), - ctx.subtypes.toList.map { p => - __InputValue( - oneOfInputFieldName(p.typeName.short, p.annotations), - getDescription(p.annotations), - () => p.typeclass.toType_(isInput = true), - None, - None - ) + ctx.subtypes.toList.flatMap { p => + p.typeclass.toType_(isInput = true).inputFields.getOrElse(Nil).map(_.nullable) }, Some(ctx.typeName.full), Some(List(Directive("oneOf"))), diff --git a/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala b/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala index 47b3753bff..1e6210f078 100644 --- a/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala +++ b/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala @@ -4,7 +4,7 @@ import caliban.CalibanError.ExecutionError import caliban.{ schema, CalibanError, InputValue } import caliban.Value.* import caliban.schema.macros.Macros -import caliban.schema.Annotations.{ GQLDefault, GQLName, GQLOneOfInputName, GQLValueType } +import caliban.schema.Annotations.{ GQLDefault, GQLExcluded, GQLName } import scala.deriving.Mirror import scala.compiletime.* @@ -82,33 +82,23 @@ trait CommonArgBuilderDerivation { _subTypes: => List[(String, List[Any], ArgBuilder[Any])], _traitLabel: => String ): ArgBuilder[A] = new ArgBuilder[A] { - - private lazy val builders: Map[String, (ArgBuilder[A], Boolean)] = - _subTypes.map { (label, annotations, argBuilder) => - val name = Types.oneOfInputFieldName(label, annotations) - val isValueType = annotations.exists { case GQLValueType(_) => true; case _ => false } - name -> (argBuilder.asInstanceOf[ArgBuilder[A]], isValueType) - }.toMap - private lazy val traitLabel = _traitLabel - private def error(input: InputValue) = ExecutionError(s"Invalid oneOf input $input for trait $traitLabel") + private lazy val combined: ArgBuilder[A] = + _subTypes.map(_._3).asInstanceOf[List[ArgBuilder[A]]] match { + case head :: tail => + tail.foldLeft(head)(_ orElse _).orElse((input: InputValue) => Left(inputError(input))) + case _ => + (_: InputValue) => Left(ExecutionError("OneOf Input Objects must have at least one subtype")) + } + + private def inputError(input: InputValue) = + ExecutionError(s"Invalid oneOf input $input for trait $traitLabel") def build(input: InputValue): Either[ExecutionError, A] = input match { - case InputValue.ObjectValue(fields) => - fields.toList match { - case (key, innerValue) :: Nil => - builders.get(key).toRight(error(input)).flatMap { (builder, isValueType) => - innerValue match { - case v: InputValue.ObjectValue if !isValueType => builder.build(v) - case v if isValueType => builder.build(v) - case _ => Left(error(input)) - } - } - case _ => - Left(ExecutionError("Exactly one key must be specified for oneOf inputs")) - } - case _ => Left(error(input)) + case InputValue.ObjectValue(f) if f.size == 1 => combined.build(input) + case InputValue.ObjectValue(_) => Left(ExecutionError("Exactly one key must be specified for oneOf inputs")) + case _ => Left(inputError(input)) } } @@ -116,20 +106,23 @@ trait CommonArgBuilderDerivation { _fields: => List[(String, List[Any], ArgBuilder[Any])], _annotations: => Map[String, List[Any]] )(fromProduct: Product => A): ArgBuilder[A] = new ArgBuilder[A] { - private lazy val fields = _fields + private lazy val annotations = _annotations + private lazy val fields = _fields.map { (label, _, builder) => + val labelList = annotations.get(label) + val default = labelList.flatMap(_.collectFirst { case GQLDefault(v) => v }) + val finalLabel = labelList.flatMap(_.collectFirst { case GQLName(name) => name }).getOrElse(label) + (finalLabel, default, builder) + } + def build(input: InputValue): Either[ExecutionError, A] = - fields.view.map { (label, _, builder) => + fields.view.map { (label, default, builder) => input match { - case InputValue.ObjectValue(fields) => - val labelList = annotations.get(label) - def default = labelList.flatMap(_.collectFirst { case GQLDefault(v) => v }) - val finalLabel = labelList.flatMap(_.collectFirst { case GQLName(name) => name }).getOrElse(label) - fields.get(finalLabel).fold(builder.buildMissing(default))(builder.build) + case InputValue.ObjectValue(fields) => fields.get(label).fold(builder.buildMissing(default))(builder.build) case value => builder.build(value) } - }.foldLeft[Either[ExecutionError, Tuple]](Right(EmptyTuple)) { case (acc, item) => + }.foldLeft[Either[ExecutionError, Tuple]](Right(EmptyTuple)) { (acc, item) => item match { case Right(value) => acc.map(_ :* value) case Left(e) => Left(e) diff --git a/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala index 628dea5d27..f90032f6db 100644 --- a/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala @@ -293,7 +293,8 @@ trait CommonSchemaDerivation { if (schema.optional) schema.toType_(isInput, isSubscription) else schema.toType_(isInput, isSubscription).nonNull, getDefaultValue(fieldAnnotations), - Some(getDirectives(fieldAnnotations)).filter(_.nonEmpty) + Some(getDirectives(fieldAnnotations)).filter(_.nonEmpty), + Some(info.short) ) }, Some(info.full), @@ -308,14 +309,8 @@ trait CommonSchemaDerivation { makeInputObject( Some(getInputName(annotations).getOrElse(customizeInputTypeName(getName(annotations, info)))), getDescription(annotations), - members.map { (label, annotations, schema) => - __InputValue( - oneOfInputFieldName(label, annotations), - getDescription(annotations), - () => schema.toType_(isInput = true), - None, - None - ) + members.flatMap { (_, _, schema) => + schema.toType_(isInput = true).inputFields.getOrElse(Nil).map(_.nullable) }, Some(info.full), Some(List(Directive("oneOf"))), diff --git a/core/src/main/scala/caliban/introspection/adt/__InputValue.scala b/core/src/main/scala/caliban/introspection/adt/__InputValue.scala index 44fe72fcbf..7a91a61013 100644 --- a/core/src/main/scala/caliban/introspection/adt/__InputValue.scala +++ b/core/src/main/scala/caliban/introspection/adt/__InputValue.scala @@ -3,13 +3,17 @@ package caliban.introspection.adt import caliban.parsing.adt.Definition.TypeSystemDefinition.TypeDefinition.InputValueDefinition import caliban.parsing.adt.Directive import caliban.parsing.Parser +import caliban.schema.Annotations.GQLExcluded + +import scala.annotation.tailrec case class __InputValue( name: String, description: Option[String], `type`: () => __Type, defaultValue: Option[String], - directives: Option[List[Directive]] = None + directives: Option[List[Directive]] = None, + @GQLExcluded parentTypeName: Option[String] = None ) { def toInputValueDefinition: InputValueDefinition = { val default = defaultValue.flatMap(v => Parser.parseInputValue(v).toOption) @@ -17,4 +21,19 @@ case class __InputValue( } private[caliban] lazy val _type: __Type = `type`() + + /** + * Makes the [[`type`]] nullable as required by the spec for OneOf Input Objects + */ + private[caliban] def nullable: __InputValue = { + @tailrec + def loop(tpe: __Type): __Type = + (tpe.kind, tpe.ofType) match { + case (__TypeKind.NON_NULL, Some(inner)) => loop(inner) + case _ => tpe + } + + val t = loop(_type) + copy(`type` = () => t) + } } diff --git a/core/src/main/scala/caliban/schema/Annotations.scala b/core/src/main/scala/caliban/schema/Annotations.scala index ec0952b29d..b92f8fe2a2 100644 --- a/core/src/main/scala/caliban/schema/Annotations.scala +++ b/core/src/main/scala/caliban/schema/Annotations.scala @@ -69,8 +69,4 @@ object Annotations { */ case class GQLOneOfInput() extends StaticAnnotation - /** - * Annotation used to rename the field name of a @oneOf input - */ - case class GQLOneOfInputName(value: String) extends StaticAnnotation } diff --git a/core/src/main/scala/caliban/schema/Types.scala b/core/src/main/scala/caliban/schema/Types.scala index b3a766430b..e5b8f656d1 100644 --- a/core/src/main/scala/caliban/schema/Types.scala +++ b/core/src/main/scala/caliban/schema/Types.scala @@ -2,7 +2,6 @@ package caliban.schema import caliban.introspection.adt._ import caliban.parsing.adt.Directive -import caliban.schema.Annotations.GQLOneOfInputName import scala.annotation.tailrec @@ -215,10 +214,4 @@ object Types { case _ => t.name }).getOrElse("") - private[caliban] def oneOfInputFieldName(typeName: String, annotations: Seq[Any]): String = - annotations.collectFirst { case GQLOneOfInputName(name) => name }.getOrElse { - val s = typeName.toCharArray - s(0) = s(0).toLower - new String(s) - } } diff --git a/core/src/main/scala/caliban/validation/Validator.scala b/core/src/main/scala/caliban/validation/Validator.scala index 69195e29a2..91c2a98723 100644 --- a/core/src/main/scala/caliban/validation/Validator.scala +++ b/core/src/main/scala/caliban/validation/Validator.scala @@ -367,10 +367,14 @@ object Validator { ) } *> ZPure.when(t.flatMap(_.isOneOf).getOrElse(false)) { - validateOneOfInputValue( - context.variables.getOrElse(v.name, NullValue), - s"Variable '${v.name}'" - ) + failValidationWhen(v.variableType.nullable)( + s"Variable '${v.name}' cannot be nullable.", + "Variables used for OneOf Input Object fields must be non-nullable." + ) *> + validateOneOfInputValue( + context.variables.getOrElse(v.name, NullValue), + s"Variable '${v.name}'" + ) } } *> { val variableUsages = collectVariablesUsed(context, op.selectionSet) @@ -543,7 +547,7 @@ object Validator { fieldArgs.getOrElse(NullValue), s"Argument '${arg.name}' on field '${field.name}'" ) - } *> ZPure.whenCase(arg.defaultValue, fieldArgs) { + } *> ZPure.whenCase((arg.defaultValue, fieldArgs)) { case (None, None) | (None, Some(NullValue)) => failValidation( s"Required argument '${arg.name}' is null or missing on field '${field.name}' of type '${currentType.name @@ -639,8 +643,8 @@ object Validator { case _ => None }) { case None | Some(NullValue) => failValidation( - s"$errorContext is not a valid @oneOf input", - "@oneOf input arguments must specify exactly one non-null key" + s"$errorContext is not a valid OneOf Input Object", + "OneOf Input Object arguments must specify exactly one non-null key" ) } .unit @@ -856,34 +860,41 @@ object Validator { private[caliban] def validateInputObject(t: __Type): EReader[Any, ValidationError, Unit] = { val inputObjectContext = s"""InputObject '${t.name.getOrElse("")}'""" - def noDuplicateInputValueName( - inputValues: List[__InputValue], - errorContext: String - ): EReader[Any, ValidationError, Unit] = { - val messageBuilder = (i: __InputValue) => s"$errorContext has repeated fields: ${i.name}" + def noDuplicateInputValueName(inputValues: List[__InputValue]): EReader[Any, ValidationError, Unit] = { + val messageBuilder = (i: __InputValue) => s"$inputObjectContext has repeated fields: ${i.name}" val explanatory = "The input field must have a unique name within that Input Object type; no two input fields may share the same name" noDuplicateName[__InputValue](inputValues, _.name, messageBuilder, explanatory) } + def noDuplicatedOneOfOrigin(inputValues: List[__InputValue]): EReader[Any, ValidationError, Unit] = { + val resolveOrigin = (i: __InputValue) => i.parentTypeName.getOrElse("") + val messageBuilder = (i: __InputValue) => + s"OneOf $inputObjectContext has multiple arguments from the same case class: ${resolveOrigin(i)}" + val explanatory = "All case classes used as arguments to OneOf Input Objects must have exactly one field" + noDuplicateName[__InputValue](inputValues, resolveOrigin, messageBuilder, explanatory) + } + def validateFields(fields: List[__InputValue]): EReader[Any, ValidationError, Unit] = ZPure.foreachDiscard(fields)(validateInputValue(_, inputObjectContext)) *> - noDuplicateInputValueName(fields, inputObjectContext) + noDuplicateInputValueName(fields) def validateOneOfFields(fields: List[__InputValue]): EReader[Any, ValidationError, Unit] = failValidationWhen(fields.size <= 1)( - s"$inputObjectContext @oneOf has less than 2 fields", - "@oneOf inputs must have at least 2 possible fields" - ) *> ZPure.foreachDiscard(fields) { f => - failValidationWhen(f.defaultValue.isDefined)( - s"$inputObjectContext @oneOf argument has a default value", - "@oneOf input fields cannot have default values" - ) *> - failValidationWhen(!f._type.isNullable)( - s"$inputObjectContext @oneOf argument is not nullable", - "All of @oneOf input fields must be nullable according to the spec" - ) - } + s"OneOf $inputObjectContext has less than 2 fields", + "OneOf input objects must have at least 2 possible fields" + ) *> + noDuplicatedOneOfOrigin(fields) *> + ZPure.foreachDiscard(fields) { f => + failValidationWhen(f.defaultValue.isDefined)( + s"OneOf $inputObjectContext argument has a default value", + "Fields of OneOf input objects cannot have default values" + ) *> + failValidationWhen(!f._type.isNullable)( + s"OneOf $inputObjectContext argument is not nullable", + "All of OneOf input fields must be declared as nullable in the schema according to the spec" + ) + } t.inputFields match { case None | Some(Nil) => diff --git a/core/src/test/scala/caliban/RenderingSpec.scala b/core/src/test/scala/caliban/RenderingSpec.scala index f5d2b2a451..aeb7f474dc 100644 --- a/core/src/test/scala/caliban/RenderingSpec.scala +++ b/core/src/test/scala/caliban/RenderingSpec.scala @@ -13,7 +13,7 @@ import caliban.parsing.adt.Definition.TypeSystemDefinition.TypeDefinition.{ import caliban.parsing.adt.Definition.{ TypeSystemDefinition, TypeSystemExtension } import caliban.parsing.adt.{ Definition, Directive } import caliban.rendering.DocumentRenderer -import caliban.schema.Annotations.{ GQLOneOfInput, GQLOneOfInputName, GQLValueType } +import caliban.schema.Annotations.GQLOneOfInput import caliban.schema.Schema.auto._ import caliban.schema.ArgBuilder.auto._ import caliban.schema.{ ArgBuilder, Schema } @@ -173,24 +173,24 @@ object RenderingSpec extends ZIOSpecDefault { rendered == """schema{query:Query} "Description of custom scalar emphasizing proper captain ship names" scalar CaptainShipName @specifiedBy(url:"http://someUrl") @tag union Role @uniondirective=Captain|Engineer|Mechanic|Pilot enum Origin @enumdirective{BELT,EARTH,MARS,MOON @deprecated(reason:"Use: EARTH | MARS | BELT")} input CharacterInput @inputobjdirective{name:String! @external nicknames:[String!]! @required origin:Origin!}interface Human{ name:String! @external}type Captain{ shipName:CaptainShipName!}type Character implements Human @key(name:"name"){ name:String! @external nicknames:[String!]! @required origin:Origin! role:Role}type Engineer{ shipName:String!}type Mechanic{ shipName:String!}type Narrator implements Human{ name:String!}type Pilot{ shipName:String!}"Queries" type Query{ "Return all characters from a given origin" characters(origin:Origin):[Character!]! character(name:String!):Character @deprecated(reason:"Use `characters`") charactersIn(names:[String!]! @lowercase):[Character!]! exists(character:CharacterInput!):Boolean! human:Human!}""" ) }, - suite("@oneOf inputs") { + suite("OneOf input objects") { def expected(label: String) = s"""schema { | query: Queries |} | |input FooInput @oneOf { - | intValue: FooIntInput | stringValue: String - | otherIntField: OtherIntFieldInput | otherStringField: String + | intValue: FooIntInput + | otherIntField: FooInt2Input |} | - |input FooIntInput { + |input FooInt2Input { | intValue: Int! |} | - |input OtherIntFieldInput { + |input FooIntInput { | intValue: Int! |} | @@ -223,18 +223,21 @@ object RenderingSpec extends ZIOSpecDefault { sealed trait Foo object Foo { - @GQLValueType - @GQLOneOfInputName("stringValue") - case class FooString(stringValue: String) extends Foo - @GQLValueType - case class OtherStringField(someField: String) extends Foo - @GQLOneOfInputName("intValue") - case class FooInt(intValue: Int) extends Foo - case class OtherIntField(intValue: Int) extends Foo + case class ArgA(stringValue: String) extends Foo + case class ArgB(otherStringField: String) extends Foo + case class ArgC(intValue: FooInt) extends Foo + case class ArgD(otherIntField: FooInt2) extends Foo + + case class FooInt(intValue: Int) + case class FooInt2(intValue: Int) case class Wrapped(fooInput: Foo) } - implicit val fooAb: ArgBuilder[Foo] = ArgBuilder.gen - implicit val fooSchema: Schema[Any, Foo] = Schema.gen + implicit val fooIntAb: ArgBuilder[Foo.FooInt] = ArgBuilder.gen + implicit val fooInt2Ab: ArgBuilder[Foo.FooInt2] = ArgBuilder.gen + implicit val fooAb: ArgBuilder[Foo] = ArgBuilder.gen + implicit val fooIntSchema: Schema[Any, Foo.FooInt] = Schema.gen + implicit val fooInt2Schema: Schema[Any, Foo.FooInt2] = Schema.gen + implicit val fooSchema: Schema[Any, Foo] = Schema.gen } diff --git a/core/src/test/scala/caliban/TestUtils.scala b/core/src/test/scala/caliban/TestUtils.scala index d467c94acb..f9011e0c1d 100644 --- a/core/src/test/scala/caliban/TestUtils.scala +++ b/core/src/test/scala/caliban/TestUtils.scala @@ -106,11 +106,7 @@ object TestUtils { @GQLOneOfInput sealed trait NameOrOrigin object NameOrOrigin { - @GQLValueType - @GQLOneOfInputName("name") - case class ByName(name: String) extends NameOrOrigin - @GQLValueType - @GQLOneOfInputName("origin") + case class ByName(name: String) extends NameOrOrigin case class ByOrigin(origin: Origin) extends NameOrOrigin case class Wrapper(nameOrOrigin: NameOrOrigin) diff --git a/core/src/test/scala/caliban/execution/ExecutionSpec.scala b/core/src/test/scala/caliban/execution/ExecutionSpec.scala index a79665b6ee..2cceee01de 100644 --- a/core/src/test/scala/caliban/execution/ExecutionSpec.scala +++ b/core/src/test/scala/caliban/execution/ExecutionSpec.scala @@ -8,7 +8,7 @@ import caliban.TestUtils._ import caliban.Value.{ BooleanValue, IntValue, NullValue, StringValue } import caliban.introspection.adt.__Type import caliban.parsing.adt.LocationInfo -import caliban.schema.Annotations.{ GQLInterface, GQLName, GQLOneOfInput, GQLOneOfInputName, GQLValueType } +import caliban.schema.Annotations.{ GQLInterface, GQLName, GQLOneOfInput, GQLValueType } import caliban.schema._ import caliban.schema.Schema.auto._ import caliban.schema.ArgBuilder.auto._ @@ -1254,12 +1254,8 @@ object ExecutionSpec extends ZIOSpecDefault { @GQLOneOfInput sealed trait Foo object Foo { - @GQLValueType - @GQLOneOfInputName("stringValue") case class FooString(stringValue: String) extends Foo - @GQLValueType - @GQLOneOfInputName("intValue") - case class FooInt(intValue: Int) extends Foo + case class FooInt(intValue: Int) extends Foo case class Wrapper(fooInput: Foo) } diff --git a/core/src/test/scala/caliban/introspection/IntrospectionSpec.scala b/core/src/test/scala/caliban/introspection/IntrospectionSpec.scala index d57c47fdc3..68d99874ad 100644 --- a/core/src/test/scala/caliban/introspection/IntrospectionSpec.scala +++ b/core/src/test/scala/caliban/introspection/IntrospectionSpec.scala @@ -158,7 +158,7 @@ object IntrospectionSpec extends ZIOSpecDefault { ) } }, - test("introspect schema with oneOf") { + test("introspect schema with OneOf input objects") { val interpreter = graphQL(resolverOneOf).interpreter interpreter.flatMap(_.execute(fullIntrospectionQuery)).map { response => @@ -196,7 +196,7 @@ object IntrospectionSpec extends ZIOSpecDefault { assertTrue(response.data.toString == """{"__type":null}""") } }, - test("introspect oneOf types") { + test("introspect OneOf input object types") { val interpreter = graphQL(resolverOneOf).interpreter def query(name: String) = diff --git a/core/src/test/scala/caliban/schema/ArgBuilderSpec.scala b/core/src/test/scala/caliban/schema/ArgBuilderSpec.scala index 882daa9a18..d50a759383 100644 --- a/core/src/test/scala/caliban/schema/ArgBuilderSpec.scala +++ b/core/src/test/scala/caliban/schema/ArgBuilderSpec.scala @@ -5,7 +5,7 @@ import caliban.InputValue import caliban.InputValue.ObjectValue import caliban.schema.ArgBuilder.auto._ import caliban.Value.{ IntValue, NullValue, StringValue } -import caliban.schema.Annotations.{ GQLOneOfInput, GQLOneOfInputName, GQLValueType } +import caliban.schema.Annotations.GQLOneOfInput import zio.test.Assertion._ import zio.test._ @@ -103,32 +103,31 @@ object ArgBuilderSpec extends ZIOSpecDefault { sealed trait Foo object Foo { - @GQLValueType - @GQLOneOfInputName("stringValue") - case class FooString(stringValue: String) extends Foo + case class Arg1(stringValue: String) extends Foo + case class Arg2(fooString2: String) extends Foo + case class Arg3(intValue: Foo1) extends Foo + case class Arg4(fooInt2: Foo2) extends Foo - @GQLValueType - case class FooString2(someField: String) extends Foo - - @GQLOneOfInputName("intValue") - case class FooInt(foo1: Int) extends Foo - - case class FooInt2(foo2: Int) extends Foo } - implicit val f1Ab: ArgBuilder[Foo.FooString] = ArgBuilder.gen - implicit val f2Ab: ArgBuilder[Foo.FooString2] = ArgBuilder.gen - implicit val f3Ab: ArgBuilder[Foo.FooInt] = ArgBuilder.gen - implicit val f4Ab: ArgBuilder[Foo.FooInt2] = ArgBuilder.gen - val fooAb: ArgBuilder[Foo] = ArgBuilder.gen + case class Foo1(foo1: Int) + case class Foo2(foo2: Int) + + implicit val foo1Ab: ArgBuilder[Foo1] = ArgBuilder.gen + implicit val foo2Ab: ArgBuilder[Foo2] = ArgBuilder.gen + implicit val f1Ab: ArgBuilder[Foo.Arg1] = ArgBuilder.gen + implicit val f2Ab: ArgBuilder[Foo.Arg2] = ArgBuilder.gen + implicit val f3Ab: ArgBuilder[Foo.Arg3] = ArgBuilder.gen + implicit val f4Ab: ArgBuilder[Foo.Arg4] = ArgBuilder.gen + val fooAb: ArgBuilder[Foo] = ArgBuilder.gen List( test("valid input") { val inputs = List( - Map("stringValue" -> StringValue("foo")) -> Foo.FooString("foo"), - Map("fooString2" -> StringValue("foo2")) -> Foo.FooString2("foo2"), - Map("intValue" -> ObjectValue(Map("foo1" -> IntValue(42)))) -> Foo.FooInt(42), - Map("fooInt2" -> ObjectValue(Map("foo2" -> IntValue(42)))) -> Foo.FooInt2(42) + Map("stringValue" -> StringValue("foo")) -> Foo.Arg1("foo"), + Map("fooString2" -> StringValue("foo2")) -> Foo.Arg2("foo2"), + Map("intValue" -> ObjectValue(Map("foo1" -> IntValue(42)))) -> Foo.Arg3(Foo1(42)), + Map("fooInt2" -> ObjectValue(Map("foo2" -> IntValue(24)))) -> Foo.Arg4(Foo2(24)) ) inputs.foldLeft(assertCompletes) { case (acc, (input, expected)) => @@ -139,7 +138,7 @@ object ArgBuilderSpec extends ZIOSpecDefault { List( Map("invalid" -> StringValue("foo")), Map("stringValue" -> StringValue("foo"), "fooString2" -> StringValue("foo2")), - Map("intValue" -> IntValue(42)), + Map("stringValue" -> ObjectValue(Map("value" -> StringValue("foo")))), Map("stringValue" -> NullValue), Map("stringValue" -> StringValue("foo"), "invalid" -> NullValue) ) diff --git a/core/src/test/scala/caliban/validation/ValidationSchemaSpec.scala b/core/src/test/scala/caliban/validation/ValidationSchemaSpec.scala index 16d5c7dde6..afc2495369 100644 --- a/core/src/test/scala/caliban/validation/ValidationSchemaSpec.scala +++ b/core/src/test/scala/caliban/validation/ValidationSchemaSpec.scala @@ -5,7 +5,8 @@ import caliban._ import caliban.TestUtils._ import caliban.TestUtils.InvalidSchemas._ import caliban.introspection.adt._ -import caliban.schema.Types +import caliban.schema.Annotations.{ GQLDefault, GQLOneOfInput } +import caliban.schema.{ ArgBuilder, Schema, Types } import caliban.schema.Schema.auto._ import caliban.schema.ArgBuilder.auto._ import caliban.{ GraphQL, RootResolver } @@ -328,6 +329,67 @@ object ValidationSchemaSpec extends ZIOSpecDefault { ) } ) - } + }, + suite("OneOf input objects")( + test("must have 2 or more possible arguments") { + @GQLOneOfInput + sealed trait Foo + case class ArgA(arg: String) extends Foo + case class Queries(foo: Foo => String) + + implicit val ab: ArgBuilder[Foo] = ArgBuilder.gen + implicit val schema: Schema[Any, Queries] = Schema.gen + + check( + graphQL(RootResolver(Queries(_.toString))), + "OneOf InputObject 'FooInput' has less than 2 fields" + ) + }, + test("must have 2 or more possible arguments") { + @GQLOneOfInput + sealed trait Foo + case class ArgA(foo: String, bar: String) extends Foo + case class ArgB(baz: String) extends Foo + case class Queries(foo: Foo => String) + + implicit val ab: ArgBuilder[Foo] = ArgBuilder.gen + implicit val schema: Schema[Any, Queries] = Schema.gen + + check( + graphQL(RootResolver(Queries(_.toString))), + "OneOf InputObject 'FooInput' has multiple arguments from the same case class: ArgA" + ) + }, + test("cannot have default values") { + @GQLOneOfInput + sealed trait Foo + case class ArgA(@GQLDefault("foo") foo: String) extends Foo + case class ArgB(bar: String) extends Foo + case class Queries(foo: Foo => String) + + implicit val ab: ArgBuilder[Foo] = ArgBuilder.gen + implicit val schema: Schema[Any, Queries] = Schema.gen + + check( + graphQL(RootResolver(Queries(_.toString))), + "OneOf InputObject 'FooInput' argument has a default value" + ) + }, + test("must have unique field names") { + @GQLOneOfInput + sealed trait Foo + case class ArgA(foo: Int) extends Foo + case class ArgB(foo: String) extends Foo + case class Queries(foo: Foo => String) + + implicit val ab: ArgBuilder[Foo] = ArgBuilder.gen + implicit val schema: Schema[Any, Queries] = Schema.gen + + check( + graphQL(RootResolver(Queries(_.toString))), + "InputObject 'FooInput' has repeated fields: foo" + ) + } + ) ) } diff --git a/core/src/test/scala/caliban/validation/ValidationSpec.scala b/core/src/test/scala/caliban/validation/ValidationSpec.scala index b647bf45a0..c23e54b34f 100644 --- a/core/src/test/scala/caliban/validation/ValidationSpec.scala +++ b/core/src/test/scala/caliban/validation/ValidationSpec.scala @@ -6,7 +6,7 @@ import caliban.InputValue.ObjectValue import caliban.Macros.gqldoc import caliban.TestUtils._ import caliban.Value.{ BooleanValue, IntValue, NullValue, StringValue } -import caliban.schema.Annotations.{ GQLDefault, GQLOneOfInput, GQLOneOfInputName, GQLValueType } +import caliban.schema.Annotations.{ GQLDefault, GQLOneOfInput } import caliban.schema.{ ArgBuilder, Schema } import zio.{ IO, UIO, ZIO } import zio.test.Assertion._ @@ -386,25 +386,25 @@ object ValidationSpec extends ZIOSpecDefault { assertTrue(response.errors.isEmpty) } }, - suite("@oneOf inputs") { + suite("OneOf input objects") { import caliban.schema.Schema.auto._ import caliban.schema.ArgBuilder.auto._ @GQLOneOfInput sealed trait Foo object Foo { - @GQLValueType - case class FooString(stringValue: String) extends Foo - @GQLOneOfInputName("fooInt") - case class IntObj(intValue: Int) extends Foo + case class FooString(fooString: String) extends Foo + case class FooInt(fooInt: IntObj) extends Foo case class Wrapper(fooInput: Foo) } + case class IntObj(intValue: Int) case class Bar(foo2: Foo => String) case class Queries(foo: Foo.Wrapper => String, fooUnwrapped: Foo => String, bar: Bar) + implicit val intObjAb: ArgBuilder[IntObj] = ArgBuilder.gen implicit val fooStringAb: ArgBuilder[Foo.FooString] = ArgBuilder.gen - implicit val fooIntAb: ArgBuilder[Foo.IntObj] = ArgBuilder.gen + implicit val fooIntAb: ArgBuilder[Foo.FooInt] = ArgBuilder.gen implicit val fooAb: ArgBuilder[Foo] = ArgBuilder.gen implicit val barSchema: Schema[Any, Bar] = Schema.gen implicit val schema: Schema[Any, Queries] = Schema.gen @@ -484,7 +484,7 @@ object ValidationSpec extends ZIOSpecDefault { .map(resp => acc && assertTrue( resp.errors.nonEmpty && resp.errors.forall { - case ValidationError(msg, _, _, _) => msg.contains("is not a valid @oneOf input") + case ValidationError(msg, _, _, _) => msg.contains("is not a valid OneOf Input Object") case _ => false } ) @@ -503,30 +503,35 @@ object ValidationSpec extends ZIOSpecDefault { .map(resp => acc && assertTrue( resp.errors.nonEmpty && resp.errors.forall { - case ValidationError(msg, _, _, _) => msg.contains("is not a valid @oneOf input") + case ValidationError(msg, _, _, _) => msg.contains("is not a valid OneOf Input Object") case _ => false } ) ) } }, + test("OneOf variables cannot be nullable") { + val variablesQuery = + """ + |query Foo($args1: FooInput){ + | foo(fooInput: $args1) + |} + |""".stripMargin + + api.interpreter + .flatMap(_.execute(variablesQuery, variables = Map("args1" -> validInputs.head))) + .map(resp => + assertTrue( + resp.errors.nonEmpty, + resp.errors.forall { + case ValidationError("Variable 'args1' cannot be nullable.", _, _, _) => true + case _ => false + } + ) + ) + }, test("schema is valid") { api.validateRootSchema.as(assertCompletes) - }, - test("schema is invalid") { - @GQLOneOfInput - sealed trait Foo - object Foo { - case class Bar(value: String) extends Foo - } - - case class Queries(foo: Foo => String) - implicit val abInner: ArgBuilder[Foo.Bar] = ArgBuilder.gen - implicit val ab: ArgBuilder[Foo] = ArgBuilder.gen - implicit val schema: Schema[Any, Queries] = Schema.gen - - graphQL(RootResolver(Queries(_.toString))).validateRootSchema - .fold(_ => assertCompletes, _ => assertNever("Schema should be invalid")) } ) } From a4f34ea3a763f1a377f4ffd7a78952f7bb8d6446 Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Tue, 29 Aug 2023 10:00:22 +0300 Subject: [PATCH 14/25] fmt --- core/src/main/scala-2/caliban/schema/SchemaDerivation.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala index 911558b846..28e9221f4f 100644 --- a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala @@ -56,7 +56,7 @@ trait CommonSchemaDerivation[R] { else p.typeclass.toType_(isInput, isSubscription).nonNull, p.annotations.collectFirst { case GQLDefault(v) => v }, Some(p.annotations.collect { case GQLDirective(dir) => dir }.toList).filter(_.nonEmpty), - Some(ctx.typeName.short), + Some(ctx.typeName.short) ) ) .toList, From 28d1413bbb2b458af5f2f64570a4e788cb33014a Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Tue, 29 Aug 2023 11:26:32 +0300 Subject: [PATCH 15/25] Allow OneOf inputs to have a single field --- .../main/scala/caliban/validation/Validator.scala | 14 +++++--------- .../caliban/validation/ValidationSchemaSpec.scala | 10 +++++----- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/core/src/main/scala/caliban/validation/Validator.scala b/core/src/main/scala/caliban/validation/Validator.scala index 91c2a98723..bd824c94fd 100644 --- a/core/src/main/scala/caliban/validation/Validator.scala +++ b/core/src/main/scala/caliban/validation/Validator.scala @@ -858,7 +858,7 @@ object Validator { } private[caliban] def validateInputObject(t: __Type): EReader[Any, ValidationError, Unit] = { - val inputObjectContext = s"""InputObject '${t.name.getOrElse("")}'""" + val inputObjectContext = s"""${if (t._isOneOfInput) "OneOf " else ""}InputObject '${t.name.getOrElse("")}'""" def noDuplicateInputValueName(inputValues: List[__InputValue]): EReader[Any, ValidationError, Unit] = { val messageBuilder = (i: __InputValue) => s"$inputObjectContext has repeated fields: ${i.name}" @@ -870,7 +870,7 @@ object Validator { def noDuplicatedOneOfOrigin(inputValues: List[__InputValue]): EReader[Any, ValidationError, Unit] = { val resolveOrigin = (i: __InputValue) => i.parentTypeName.getOrElse("") val messageBuilder = (i: __InputValue) => - s"OneOf $inputObjectContext has multiple arguments from the same case class: ${resolveOrigin(i)}" + s"$inputObjectContext has multiple arguments from the same case class: ${resolveOrigin(i)}" val explanatory = "All case classes used as arguments to OneOf Input Objects must have exactly one field" noDuplicateName[__InputValue](inputValues, resolveOrigin, messageBuilder, explanatory) } @@ -880,18 +880,14 @@ object Validator { noDuplicateInputValueName(fields) def validateOneOfFields(fields: List[__InputValue]): EReader[Any, ValidationError, Unit] = - failValidationWhen(fields.size <= 1)( - s"OneOf $inputObjectContext has less than 2 fields", - "OneOf input objects must have at least 2 possible fields" - ) *> - noDuplicatedOneOfOrigin(fields) *> + noDuplicatedOneOfOrigin(fields) *> ZPure.foreachDiscard(fields) { f => failValidationWhen(f.defaultValue.isDefined)( - s"OneOf $inputObjectContext argument has a default value", + s"$inputObjectContext argument has a default value", "Fields of OneOf input objects cannot have default values" ) *> failValidationWhen(!f._type.isNullable)( - s"OneOf $inputObjectContext argument is not nullable", + s"$inputObjectContext argument is not nullable", "All of OneOf input fields must be declared as nullable in the schema according to the spec" ) } diff --git a/core/src/test/scala/caliban/validation/ValidationSchemaSpec.scala b/core/src/test/scala/caliban/validation/ValidationSchemaSpec.scala index afc2495369..55e2e66178 100644 --- a/core/src/test/scala/caliban/validation/ValidationSchemaSpec.scala +++ b/core/src/test/scala/caliban/validation/ValidationSchemaSpec.scala @@ -331,10 +331,10 @@ object ValidationSchemaSpec extends ZIOSpecDefault { ) }, suite("OneOf input objects")( - test("must have 2 or more possible arguments") { + test("must have at least one input field") { @GQLOneOfInput sealed trait Foo - case class ArgA(arg: String) extends Foo + case object ArgA extends Foo case class Queries(foo: Foo => String) implicit val ab: ArgBuilder[Foo] = ArgBuilder.gen @@ -342,10 +342,10 @@ object ValidationSchemaSpec extends ZIOSpecDefault { check( graphQL(RootResolver(Queries(_.toString))), - "OneOf InputObject 'FooInput' has less than 2 fields" + "OneOf InputObject 'FooInput' does not have fields" ) }, - test("must have 2 or more possible arguments") { + test("case classes extending OneOf sealed traits must have a single argument") { @GQLOneOfInput sealed trait Foo case class ArgA(foo: String, bar: String) extends Foo @@ -387,7 +387,7 @@ object ValidationSchemaSpec extends ZIOSpecDefault { check( graphQL(RootResolver(Queries(_.toString))), - "InputObject 'FooInput' has repeated fields: foo" + "OneOf InputObject 'FooInput' has repeated fields: foo" ) } ) From 7dbc6fdd8e83175ab851367f975d130fae5a90da Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Thu, 21 Sep 2023 11:24:18 +1000 Subject: [PATCH 16/25] Fix merging errors and add mima exclusions --- .circleci/config.yml | 2 +- build.sbt | 4 +++- core/src/main/scala/caliban/validation/Validator.scala | 2 +- core/src/test/scala/caliban/RenderingSpec.scala | 1 - 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9d3e0b1c96..21697b89e6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -149,7 +149,7 @@ jobs: - restore_cache: key: sbtcache - run: sbt ++2.13.12 http4s/mimaReportBinaryIssues akkaHttp/mimaReportBinaryIssues pekkoHttp/mimaReportBinaryIssues play/mimaReportBinaryIssues zioHttp/mimaReportBinaryIssues catsInterop/mimaReportBinaryIssues monixInterop/mimaReportBinaryIssues clientJVM/mimaReportBinaryIssues clientJS/mimaReportBinaryIssues clientLaminextJS/mimaReportBinaryIssues federation/mimaReportBinaryIssues reporting/mimaReportBinaryIssues tracing/mimaReportBinaryIssues tools/mimaReportBinaryIssues core/mimaReportBinaryIssues tapirInterop/mimaReportBinaryIssues - - run: sbt ++3.3.0 catsInterop/mimaReportBinaryIssues monixInterop/mimaReportBinaryIssues clientJVM/mimaReportBinaryIssues clientJS/mimaReportBinaryIssues clientLaminextJS/mimaReportBinaryIssues zioHttp/mimaReportBinaryIssues http4s/mimaReportBinaryIssues federation/mimaReportBinaryIssues reporting/mimaReportBinaryIssues tracing/mimaReportBinaryIssues tools/mimaReportBinaryIssues core/mimaReportBinaryIssues tapirInterop/mimaReportBinaryIssues pekkoHttp/mimaReportBinaryIssues + - run: sbt ++3.3.1 catsInterop/mimaReportBinaryIssues monixInterop/mimaReportBinaryIssues clientJVM/mimaReportBinaryIssues clientJS/mimaReportBinaryIssues clientLaminextJS/mimaReportBinaryIssues zioHttp/mimaReportBinaryIssues http4s/mimaReportBinaryIssues federation/mimaReportBinaryIssues reporting/mimaReportBinaryIssues tracing/mimaReportBinaryIssues tools/mimaReportBinaryIssues core/mimaReportBinaryIssues tapirInterop/mimaReportBinaryIssues pekkoHttp/mimaReportBinaryIssues - save_cache: key: sbtcache paths: diff --git a/build.sbt b/build.sbt index 774819768f..d092f0d1fd 100644 --- a/build.sbt +++ b/build.sbt @@ -662,7 +662,9 @@ lazy val enableMimaSettingsJVM = ProblemFilters.exclude[MissingClassProblem]("caliban.GraphQLResponseJsonCompat"), ProblemFilters.exclude[MissingClassProblem]("caliban.GraphQLRequestJsonCompat"), ProblemFilters.exclude[MissingClassProblem]("caliban.CalibanErrorJsonCompat"), - ProblemFilters.exclude[MissingClassProblem]("caliban.ValueJsonCompat") + ProblemFilters.exclude[MissingClassProblem]("caliban.ValueJsonCompat"), + ProblemFilters.exclude[DirectMissingMethodProblem]("caliban.introspection.adt.__InputValue.*"), + ProblemFilters.exclude[DirectMissingMethodProblem]("caliban.introspection.adt.__Type.*") ) ) diff --git a/core/src/main/scala/caliban/validation/Validator.scala b/core/src/main/scala/caliban/validation/Validator.scala index 258041efc9..f727201142 100644 --- a/core/src/main/scala/caliban/validation/Validator.scala +++ b/core/src/main/scala/caliban/validation/Validator.scala @@ -887,7 +887,7 @@ object Validator { def validateFields(fields: List[__InputValue]): EReader[Any, ValidationError, Unit] = ZPure.foreachDiscard(fields)(validateInputValue(_, inputObjectContext)) *> - noDuplicateInputValueName(fields) + noDuplicateInputValueName(fields, inputObjectContext) def validateOneOfFields(fields: List[__InputValue]): EReader[Any, ValidationError, Unit] = noDuplicatedOneOfOrigin(fields) *> diff --git a/core/src/test/scala/caliban/RenderingSpec.scala b/core/src/test/scala/caliban/RenderingSpec.scala index 51ec2d9c5f..441ea8ae4c 100644 --- a/core/src/test/scala/caliban/RenderingSpec.scala +++ b/core/src/test/scala/caliban/RenderingSpec.scala @@ -236,7 +236,6 @@ object RenderingSpec extends ZIOSpecDefault { reparsed <- Parser.parseQuery(rendered).either } yield assertTrue(input == rendered, reparsed.isRight)) - @GQLOneOfInput sealed trait Foo From ab94d86670250926a5d26734a1ece5287dec6423 Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Sun, 15 Oct 2023 14:44:36 +1100 Subject: [PATCH 17/25] Fix merging errors --- .../caliban/schema/SchemaDerivation.scala | 2 +- .../caliban/schema/SchemaDerivation.scala | 15 +++---- .../caliban/introspection/Introspector.scala | 2 +- .../main/scala/caliban/wrappers/Caching.scala | 39 ++++++++++--------- .../caliban/execution/DefaultValueSpec.scala | 6 +-- .../introspection/IntrospectionSpec.scala | 30 +++++++------- 6 files changed, 46 insertions(+), 48 deletions(-) diff --git a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala index 663c8e0bbd..ac416ec021 100644 --- a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala @@ -168,7 +168,7 @@ trait CommonSchemaDerivation[R] { .getOrElse(customizeInputTypeName(getName(ctx)))), getDescription(ctx), ctx.subtypes.toList.flatMap { p => - p.typeclass.toType_(isInput = true).inputFields.getOrElse(Nil).map(_.nullable) + p.typeclass.toType_(isInput = true).allInputFields.map(_.nullable) }, Some(ctx.typeName.full), Some(List(Directive("oneOf"))), diff --git a/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala index fbb31b91d9..886f598343 100644 --- a/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala @@ -87,9 +87,9 @@ trait CommonSchemaDerivation { private lazy val membersOrdered = members.sortBy(_._1).toList - private lazy val subTypes = membersOrdered.map { (label, subTypeAnnotations, schema) => + private lazy val subTypes = membersOrdered.map { (label, subTypeAnnotations, schema, _) => (label, schema.toType_(), subTypeAnnotations) - }.sortBy { case (label, _, _) => label }.toList + }.toList private lazy val isEnum = subTypes.forall { (_, t, _) => t.allFields.isEmpty && t.allInputFields.isEmpty @@ -108,10 +108,9 @@ trait CommonSchemaDerivation { private lazy val isOneOfInput = annotations.contains(GQLOneOfInput()) def toType(isInput: Boolean, isSubscription: Boolean): __Type = - if (!isInterface && !isUnion && membersOrdered.nonEmpty && isEnum && !isOneOfInput) - mkEnum(annotations, info, subTypes) + if (!isInterface && !isUnion && subTypes.nonEmpty && isEnum && !isOneOfInput) mkEnum(annotations, info, subTypes) else if (isOneOfInput && isInput) { - mkOneOfInput(annotations, membersOrdered, info) + mkOneOfInput(annotations, membersOrdered.map(_._3), info) } else if (!isInterface) makeUnion( Some(getName(annotations, info)), @@ -309,15 +308,13 @@ trait CommonSchemaDerivation { private def mkOneOfInput[R]( annotations: List[Any], - members: List[(String, List[Any], Schema[R, Any])], + schemas: List[Schema[R, Any]], info: TypeInfo ) = makeInputObject( Some(getInputName(annotations).getOrElse(customizeInputTypeName(getName(annotations, info)))), getDescription(annotations), - members.flatMap { (_, _, schema) => - schema.toType_(isInput = true).inputFields.getOrElse(Nil).map(_.nullable) - }, + schemas.flatMap(_.toType_(isInput = true).allInputFields.map(_.nullable)), Some(info.full), Some(List(Directive("oneOf"))), isOneOf = true diff --git a/core/src/main/scala/caliban/introspection/Introspector.scala b/core/src/main/scala/caliban/introspection/Introspector.scala index 58fc8ead84..2035ed210e 100644 --- a/core/src/main/scala/caliban/introspection/Introspector.scala +++ b/core/src/main/scala/caliban/introspection/Introspector.scala @@ -54,7 +54,7 @@ object Introspector extends IntrospectionDerivation { "The `@oneOf` directive is used within the type system definition language to indicate an Input Object is a OneOf Input Object." ), Set(__DirectiveLocation.INPUT_OBJECT), - Nil, + _ => Nil, isRepeatable = false ) diff --git a/core/src/main/scala/caliban/wrappers/Caching.scala b/core/src/main/scala/caliban/wrappers/Caching.scala index eb349df56f..42fae48c5a 100644 --- a/core/src/main/scala/caliban/wrappers/Caching.scala +++ b/core/src/main/scala/caliban/wrappers/Caching.scala @@ -28,26 +28,27 @@ object Caching { name = DirectiveName, description = None, locations = Set(__DirectiveLocation.FIELD_DEFINITION, __DirectiveLocation.OBJECT), - args = List( - __InputValue( - name = MaxAgeName, - None, - `type` = () => Types.int, - defaultValue = None - ), - __InputValue( - name = ScopeName, - None, - `type` = () => CacheScope._type, - defaultValue = None + args = _ => + List( + __InputValue( + name = MaxAgeName, + None, + `type` = () => Types.int, + defaultValue = None + ), + __InputValue( + name = ScopeName, + None, + `type` = () => CacheScope._type, + defaultValue = None + ), + __InputValue( + name = InheritMaxAgeName, + None, + `type` = () => Types.boolean, + defaultValue = None + ) ), - __InputValue( - name = InheritMaxAgeName, - None, - `type` = () => Types.boolean, - defaultValue = None - ) - ), isRepeatable = false ) ) diff --git a/core/src/test/scala/caliban/execution/DefaultValueSpec.scala b/core/src/test/scala/caliban/execution/DefaultValueSpec.scala index 3acd508cb1..d5a6d0e2d9 100644 --- a/core/src/test/scala/caliban/execution/DefaultValueSpec.scala +++ b/core/src/test/scala/caliban/execution/DefaultValueSpec.scala @@ -222,10 +222,10 @@ object DefaultValueSpec extends ZIOSpecDefault { case class Query(testDefault: TestInput => Int) val interpreter = graphQL(RootResolver(Query(i => i.intValue))).interpreter - interpreter.flatMap(_.execute(introspectionQuery)).map { response => + interpreter.flatMap(_.execute(introspectionQuery)).map(_.data.toString).map { response => assertTrue( - response.data.toString == - """{"__schema":{"queryType":{"name":"Query"},"mutationType":null,"subscriptionType":null,"types":[{"kind":"SCALAR","name":"Boolean","fields":null,"inputFields":null},{"kind":"SCALAR","name":"Float","fields":null,"inputFields":null},{"kind":"SCALAR","name":"Int","fields":null,"inputFields":null},{"kind":"OBJECT","name":"Query","fields":[{"name":"testDefault","description":null,"args":[{"name":"intValue","description":null,"defaultValue":"1"}]}],"inputFields":null},{"kind":"SCALAR","name":"String","fields":null,"inputFields":null},{"kind":"OBJECT","name":"__Directive","fields":[{"name":"name","description":null,"args":[]},{"name":"description","description":null,"args":[]},{"name":"locations","description":null,"args":[]},{"name":"args","description":null,"args":[{"name":"includeDeprecated","description":null,"defaultValue":null}]},{"name":"isRepeatable","description":null,"args":[]}],"inputFields":null},{"kind":"ENUM","name":"__DirectiveLocation","fields":null,"inputFields":null},{"kind":"OBJECT","name":"__EnumValue","fields":[{"name":"name","description":null,"args":[]},{"name":"description","description":null,"args":[]},{"name":"isDeprecated","description":null,"args":[]},{"name":"deprecationReason","description":null,"args":[]}],"inputFields":null},{"kind":"OBJECT","name":"__Field","fields":[{"name":"name","description":null,"args":[]},{"name":"description","description":null,"args":[]},{"name":"args","description":null,"args":[{"name":"includeDeprecated","description":null,"defaultValue":null}]},{"name":"type","description":null,"args":[]},{"name":"isDeprecated","description":null,"args":[]},{"name":"deprecationReason","description":null,"args":[]}],"inputFields":null},{"kind":"OBJECT","name":"__InputValue","fields":[{"name":"name","description":null,"args":[]},{"name":"description","description":null,"args":[]},{"name":"type","description":null,"args":[]},{"name":"defaultValue","description":null,"args":[]},{"name":"isDeprecated","description":null,"args":[]},{"name":"deprecationReason","description":null,"args":[]}],"inputFields":null},{"kind":"OBJECT","name":"__Schema","fields":[{"name":"description","description":null,"args":[]},{"name":"queryType","description":null,"args":[]},{"name":"mutationType","description":null,"args":[]},{"name":"subscriptionType","description":null,"args":[]},{"name":"types","description":null,"args":[]},{"name":"directives","description":null,"args":[]}],"inputFields":null},{"kind":"OBJECT","name":"__Type","fields":[{"name":"kind","description":null,"args":[]},{"name":"name","description":null,"args":[]},{"name":"description","description":null,"args":[]},{"name":"fields","description":null,"args":[{"name":"includeDeprecated","description":null,"defaultValue":null}]},{"name":"interfaces","description":null,"args":[]},{"name":"possibleTypes","description":null,"args":[]},{"name":"enumValues","description":null,"args":[{"name":"includeDeprecated","description":null,"defaultValue":null}]},{"name":"inputFields","description":null,"args":[{"name":"includeDeprecated","description":null,"defaultValue":null}]},{"name":"ofType","description":null,"args":[]},{"name":"specifiedBy","description":null,"args":[]}],"inputFields":null},{"kind":"ENUM","name":"__TypeKind","fields":null,"inputFields":null}]}}""" + response == + """{"__schema":{"queryType":{"name":"Query"},"mutationType":null,"subscriptionType":null,"types":[{"kind":"SCALAR","name":"Boolean","fields":null,"inputFields":null},{"kind":"SCALAR","name":"Float","fields":null,"inputFields":null},{"kind":"SCALAR","name":"Int","fields":null,"inputFields":null},{"kind":"OBJECT","name":"Query","fields":[{"name":"testDefault","description":null,"args":[{"name":"intValue","description":null,"defaultValue":"1"}]}],"inputFields":null},{"kind":"SCALAR","name":"String","fields":null,"inputFields":null},{"kind":"OBJECT","name":"__Directive","fields":[{"name":"name","description":null,"args":[]},{"name":"description","description":null,"args":[]},{"name":"locations","description":null,"args":[]},{"name":"args","description":null,"args":[{"name":"includeDeprecated","description":null,"defaultValue":null}]},{"name":"isRepeatable","description":null,"args":[]}],"inputFields":null},{"kind":"ENUM","name":"__DirectiveLocation","fields":null,"inputFields":null},{"kind":"OBJECT","name":"__EnumValue","fields":[{"name":"name","description":null,"args":[]},{"name":"description","description":null,"args":[]},{"name":"isDeprecated","description":null,"args":[]},{"name":"deprecationReason","description":null,"args":[]}],"inputFields":null},{"kind":"OBJECT","name":"__Field","fields":[{"name":"name","description":null,"args":[]},{"name":"description","description":null,"args":[]},{"name":"args","description":null,"args":[{"name":"includeDeprecated","description":null,"defaultValue":null}]},{"name":"type","description":null,"args":[]},{"name":"isDeprecated","description":null,"args":[]},{"name":"deprecationReason","description":null,"args":[]}],"inputFields":null},{"kind":"OBJECT","name":"__InputValue","fields":[{"name":"name","description":null,"args":[]},{"name":"description","description":null,"args":[]},{"name":"type","description":null,"args":[]},{"name":"defaultValue","description":null,"args":[]},{"name":"isDeprecated","description":null,"args":[]},{"name":"deprecationReason","description":null,"args":[]}],"inputFields":null},{"kind":"OBJECT","name":"__Schema","fields":[{"name":"description","description":null,"args":[]},{"name":"queryType","description":null,"args":[]},{"name":"mutationType","description":null,"args":[]},{"name":"subscriptionType","description":null,"args":[]},{"name":"types","description":null,"args":[]},{"name":"directives","description":null,"args":[]}],"inputFields":null},{"kind":"OBJECT","name":"__Type","fields":[{"name":"kind","description":null,"args":[]},{"name":"name","description":null,"args":[]},{"name":"description","description":null,"args":[]},{"name":"fields","description":null,"args":[{"name":"includeDeprecated","description":null,"defaultValue":null}]},{"name":"interfaces","description":null,"args":[]},{"name":"possibleTypes","description":null,"args":[]},{"name":"enumValues","description":null,"args":[{"name":"includeDeprecated","description":null,"defaultValue":null}]},{"name":"inputFields","description":null,"args":[{"name":"includeDeprecated","description":null,"defaultValue":null}]},{"name":"ofType","description":null,"args":[]},{"name":"specifiedBy","description":null,"args":[]},{"name":"isOneOf","description":null,"args":[]}],"inputFields":null},{"kind":"ENUM","name":"__TypeKind","fields":null,"inputFields":null}]}}""" ) } } diff --git a/core/src/test/scala/caliban/introspection/IntrospectionSpec.scala b/core/src/test/scala/caliban/introspection/IntrospectionSpec.scala index 8891ab050c..8303139f91 100644 --- a/core/src/test/scala/caliban/introspection/IntrospectionSpec.scala +++ b/core/src/test/scala/caliban/introspection/IntrospectionSpec.scala @@ -123,10 +123,10 @@ object IntrospectionSpec extends ZIOSpecDefault { test("introspect schema") { val interpreter = graphQL(resolverIO).interpreter - interpreter.flatMap(_.execute(fullIntrospectionQuery)).map { response => + interpreter.flatMap(_.execute(fullIntrospectionQuery)).map(_.data.toString).map { response => assertTrue( - response.data.toString == - """{"__schema":{"queryType":{"name":"QueryIO"},"mutationType":null,"subscriptionType":null,"types":[{"kind":"SCALAR","name":"Boolean","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Captain","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"CaptainShipName","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"CaptainShipName","description":"Description of custom scalar emphasizing proper captain ship names","isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Character","description":null,"isOneOf":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"nicknames","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"origin","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"Origin","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"role","description":null,"args":[],"type":{"kind":"UNION","name":"Role","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Engineer","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Float","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Int","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Mechanic","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"Origin","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"BELT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"EARTH","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MARS","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MOON","description":null,"isDeprecated":true,"deprecationReason":"Use: EARTH | MARS | BELT"}],"possibleTypes":null},{"kind":"OBJECT","name":"Pilot","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"QueryIO","description":"Queries","isOneOf":null,"fields":[{"name":"characters","description":"Return all characters from a given origin","args":[{"name":"origin","description":null,"type":{"kind":"ENUM","name":"Origin","ofType":null},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"Character","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"character","description":null,"args":[{"name":"name","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}],"type":{"kind":"OBJECT","name":"Character","ofType":null},"isDeprecated":true,"deprecationReason":"Use `characters`"}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"UNION","name":"Role","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":[{"kind":"OBJECT","name":"Captain","ofType":null},{"kind":"OBJECT","name":"Engineer","ofType":null},{"kind":"OBJECT","name":"Mechanic","ofType":null},{"kind":"OBJECT","name":"Pilot","ofType":null}]},{"kind":"SCALAR","name":"String","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Directive","description":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"locations","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"__DirectiveLocation","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"args","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__InputValue","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"isRepeatable","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"__DirectiveLocation","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"ARGUMENT_DEFINITION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"ENUM","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"ENUM_VALUE","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"FIELD","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"FIELD_DEFINITION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"FRAGMENT_DEFINITION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"FRAGMENT_SPREAD","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INLINE_FRAGMENT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INPUT_FIELD_DEFINITION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INPUT_OBJECT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INTERFACE","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MUTATION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"OBJECT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"QUERY","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"SCALAR","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"SCHEMA","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"SUBSCRIPTION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"UNION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"VARIABLE_DEFINITION","description":null,"isDeprecated":false,"deprecationReason":null}],"possibleTypes":null},{"kind":"OBJECT","name":"__EnumValue","description":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"isDeprecated","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"deprecationReason","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Field","description":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"args","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__InputValue","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"type","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"isDeprecated","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"deprecationReason","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__InputValue","description":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"type","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"defaultValue","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"isDeprecated","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"deprecationReason","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Schema","description":null,"fields":[{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"queryType","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"mutationType","description":null,"args":[],"type":{"kind":"OBJECT","name":"__Type","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"subscriptionType","description":null,"args":[],"type":{"kind":"OBJECT","name":"__Type","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"types","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"directives","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Directive","ofType":null}}}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Type","description":null,"fields":[{"name":"kind","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"__TypeKind","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"fields","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Field","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"interfaces","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"possibleTypes","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"enumValues","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__EnumValue","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"inputFields","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__InputValue","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"ofType","description":null,"args":[],"type":{"kind":"OBJECT","name":"__Type","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"specifiedBy","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"__TypeKind","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"ENUM","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INPUT_OBJECT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INTERFACE","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"LIST","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"NON_NULL","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"OBJECT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"SCALAR","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"UNION","description":null,"isDeprecated":false,"deprecationReason":null}],"possibleTypes":null}],"directives":[{"name":"skip","description":"The @skip directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional exclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"include","description":"The @include directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional inclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"specifiedBy","description":"The @specifiedBy directive is used within the type system definition language to provide a URL for specifying the behavior of custom scalar types. The URL should point to a human-readable specification of the data format, serialization, and coercion rules. It must not appear on built-in scalar types.","locations":["SCALAR"],"args":[{"name":"url","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}]}]}}""" + response == + """{"__schema":{"queryType":{"name":"QueryIO"},"mutationType":null,"subscriptionType":null,"types":[{"kind":"SCALAR","name":"Boolean","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Captain","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"CaptainShipName","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"CaptainShipName","description":"Description of custom scalar emphasizing proper captain ship names","isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Character","description":null,"isOneOf":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"nicknames","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"origin","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"Origin","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"role","description":null,"args":[],"type":{"kind":"UNION","name":"Role","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Engineer","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Float","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Int","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Mechanic","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"Origin","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"BELT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"EARTH","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MARS","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MOON","description":null,"isDeprecated":true,"deprecationReason":"Use: EARTH | MARS | BELT"}],"possibleTypes":null},{"kind":"OBJECT","name":"Pilot","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"QueryIO","description":"Queries","isOneOf":null,"fields":[{"name":"characters","description":"Return all characters from a given origin","args":[{"name":"origin","description":null,"type":{"kind":"ENUM","name":"Origin","ofType":null},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"Character","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"character","description":null,"args":[{"name":"name","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}],"type":{"kind":"OBJECT","name":"Character","ofType":null},"isDeprecated":true,"deprecationReason":"Use `characters`"}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"UNION","name":"Role","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":[{"kind":"OBJECT","name":"Captain","ofType":null},{"kind":"OBJECT","name":"Engineer","ofType":null},{"kind":"OBJECT","name":"Mechanic","ofType":null},{"kind":"OBJECT","name":"Pilot","ofType":null}]},{"kind":"SCALAR","name":"String","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Directive","description":null,"isOneOf":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"locations","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"__DirectiveLocation","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"args","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__InputValue","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"isRepeatable","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"__DirectiveLocation","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"ARGUMENT_DEFINITION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"ENUM","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"ENUM_VALUE","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"FIELD","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"FIELD_DEFINITION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"FRAGMENT_DEFINITION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"FRAGMENT_SPREAD","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INLINE_FRAGMENT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INPUT_FIELD_DEFINITION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INPUT_OBJECT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INTERFACE","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MUTATION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"OBJECT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"QUERY","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"SCALAR","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"SCHEMA","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"SUBSCRIPTION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"UNION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"VARIABLE_DEFINITION","description":null,"isDeprecated":false,"deprecationReason":null}],"possibleTypes":null},{"kind":"OBJECT","name":"__EnumValue","description":null,"isOneOf":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"isDeprecated","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"deprecationReason","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Field","description":null,"isOneOf":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"args","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__InputValue","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"type","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"isDeprecated","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"deprecationReason","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__InputValue","description":null,"isOneOf":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"type","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"defaultValue","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"isDeprecated","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"deprecationReason","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Schema","description":null,"isOneOf":null,"fields":[{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"queryType","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"mutationType","description":null,"args":[],"type":{"kind":"OBJECT","name":"__Type","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"subscriptionType","description":null,"args":[],"type":{"kind":"OBJECT","name":"__Type","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"types","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"directives","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Directive","ofType":null}}}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Type","description":null,"isOneOf":null,"fields":[{"name":"kind","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"__TypeKind","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"fields","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Field","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"interfaces","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"possibleTypes","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"enumValues","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__EnumValue","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"inputFields","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__InputValue","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"ofType","description":null,"args":[],"type":{"kind":"OBJECT","name":"__Type","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"specifiedBy","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"isOneOf","description":null,"args":[],"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"__TypeKind","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"ENUM","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INPUT_OBJECT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INTERFACE","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"LIST","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"NON_NULL","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"OBJECT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"SCALAR","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"UNION","description":null,"isDeprecated":false,"deprecationReason":null}],"possibleTypes":null}],"directives":[{"name":"skip","description":"The @skip directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional exclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"include","description":"The @include directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional inclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"specifiedBy","description":"The @specifiedBy directive is used within the type system definition language to provide a URL for specifying the behavior of custom scalar types. The URL should point to a human-readable specification of the data format, serialization, and coercion rules. It must not appear on built-in scalar types.","locations":["SCALAR"],"args":[{"name":"url","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}]}]}}""" ) } }, @@ -151,20 +151,20 @@ object IntrospectionSpec extends ZIOSpecDefault { val interpreter = (graphQL(resolverIO) @@ hideWrapper).interpreter - interpreter.flatMap(_.execute(fullIntrospectionQuery)).map { response => + interpreter.flatMap(_.execute(fullIntrospectionQuery)).map(_.data.toString).map { response => assertTrue( - response.data.toString == - """{"__schema":{"queryType":{"name":"QueryIO"},"mutationType":null,"subscriptionType":null,"types":[{"kind":"SCALAR","name":"Boolean","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Captain","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"CaptainShipName","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"CaptainShipName","description":"Description of custom scalar emphasizing proper captain ship names","isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Character","description":null,"isOneOf":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"nicknames","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"origin","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"Origin","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"role","description":null,"args":[],"type":{"kind":"UNION","name":"Role","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Engineer","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Float","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Int","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Mechanic","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"Origin","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"BELT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"EARTH","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MARS","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MOON","description":null,"isDeprecated":true,"deprecationReason":"Use: EARTH | MARS | BELT"}],"possibleTypes":null},{"kind":"OBJECT","name":"Pilot","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"QueryIO","description":"Queries","isOneOf":null,"fields":[{"name":"characters","description":"Return all characters from a given origin","args":[{"name":"origin","description":null,"type":{"kind":"ENUM","name":"Origin","ofType":null},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"Character","ofType":null}}}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"UNION","name":"Role","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":[{"kind":"OBJECT","name":"Captain","ofType":null},{"kind":"OBJECT","name":"Engineer","ofType":null},{"kind":"OBJECT","name":"Mechanic","ofType":null},{"kind":"OBJECT","name":"Pilot","ofType":null}]},{"kind":"SCALAR","name":"String","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Directive","description":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"locations","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"__DirectiveLocation","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"args","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__InputValue","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"isRepeatable","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"__DirectiveLocation","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"ARGUMENT_DEFINITION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"ENUM","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"ENUM_VALUE","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"FIELD","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"FIELD_DEFINITION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"FRAGMENT_DEFINITION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"FRAGMENT_SPREAD","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INLINE_FRAGMENT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INPUT_FIELD_DEFINITION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INPUT_OBJECT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INTERFACE","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MUTATION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"OBJECT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"QUERY","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"SCALAR","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"SCHEMA","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"SUBSCRIPTION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"UNION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"VARIABLE_DEFINITION","description":null,"isDeprecated":false,"deprecationReason":null}],"possibleTypes":null},{"kind":"OBJECT","name":"__EnumValue","description":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"isDeprecated","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"deprecationReason","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Field","description":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"args","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__InputValue","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"type","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"isDeprecated","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"deprecationReason","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__InputValue","description":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"type","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"defaultValue","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"isDeprecated","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"deprecationReason","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Schema","description":null,"fields":[{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"queryType","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"mutationType","description":null,"args":[],"type":{"kind":"OBJECT","name":"__Type","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"subscriptionType","description":null,"args":[],"type":{"kind":"OBJECT","name":"__Type","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"types","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"directives","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Directive","ofType":null}}}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Type","description":null,"fields":[{"name":"kind","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"__TypeKind","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"fields","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Field","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"interfaces","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"possibleTypes","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"enumValues","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__EnumValue","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"inputFields","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__InputValue","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"ofType","description":null,"args":[],"type":{"kind":"OBJECT","name":"__Type","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"specifiedBy","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"__TypeKind","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"ENUM","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INPUT_OBJECT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INTERFACE","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"LIST","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"NON_NULL","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"OBJECT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"SCALAR","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"UNION","description":null,"isDeprecated":false,"deprecationReason":null}],"possibleTypes":null}],"directives":[{"name":"skip","description":"The @skip directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional exclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"include","description":"The @include directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional inclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"specifiedBy","description":"The @specifiedBy directive is used within the type system definition language to provide a URL for specifying the behavior of custom scalar types. The URL should point to a human-readable specification of the data format, serialization, and coercion rules. It must not appear on built-in scalar types.","locations":["SCALAR"],"args":[{"name":"url","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}]}]}}""" + response == + """{"__schema":{"queryType":{"name":"QueryIO"},"mutationType":null,"subscriptionType":null,"types":[{"kind":"SCALAR","name":"Boolean","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Captain","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"CaptainShipName","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"CaptainShipName","description":"Description of custom scalar emphasizing proper captain ship names","isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Character","description":null,"isOneOf":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"nicknames","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"origin","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"Origin","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"role","description":null,"args":[],"type":{"kind":"UNION","name":"Role","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Engineer","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Float","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Int","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Mechanic","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"Origin","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"BELT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"EARTH","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MARS","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MOON","description":null,"isDeprecated":true,"deprecationReason":"Use: EARTH | MARS | BELT"}],"possibleTypes":null},{"kind":"OBJECT","name":"Pilot","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"QueryIO","description":"Queries","isOneOf":null,"fields":[{"name":"characters","description":"Return all characters from a given origin","args":[{"name":"origin","description":null,"type":{"kind":"ENUM","name":"Origin","ofType":null},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"Character","ofType":null}}}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"UNION","name":"Role","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":[{"kind":"OBJECT","name":"Captain","ofType":null},{"kind":"OBJECT","name":"Engineer","ofType":null},{"kind":"OBJECT","name":"Mechanic","ofType":null},{"kind":"OBJECT","name":"Pilot","ofType":null}]},{"kind":"SCALAR","name":"String","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Directive","description":null,"isOneOf":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"locations","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"__DirectiveLocation","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"args","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__InputValue","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"isRepeatable","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"__DirectiveLocation","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"ARGUMENT_DEFINITION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"ENUM","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"ENUM_VALUE","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"FIELD","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"FIELD_DEFINITION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"FRAGMENT_DEFINITION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"FRAGMENT_SPREAD","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INLINE_FRAGMENT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INPUT_FIELD_DEFINITION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INPUT_OBJECT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INTERFACE","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MUTATION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"OBJECT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"QUERY","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"SCALAR","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"SCHEMA","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"SUBSCRIPTION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"UNION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"VARIABLE_DEFINITION","description":null,"isDeprecated":false,"deprecationReason":null}],"possibleTypes":null},{"kind":"OBJECT","name":"__EnumValue","description":null,"isOneOf":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"isDeprecated","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"deprecationReason","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Field","description":null,"isOneOf":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"args","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__InputValue","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"type","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"isDeprecated","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"deprecationReason","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__InputValue","description":null,"isOneOf":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"type","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"defaultValue","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"isDeprecated","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"deprecationReason","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Schema","description":null,"isOneOf":null,"fields":[{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"queryType","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"mutationType","description":null,"args":[],"type":{"kind":"OBJECT","name":"__Type","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"subscriptionType","description":null,"args":[],"type":{"kind":"OBJECT","name":"__Type","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"types","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"directives","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Directive","ofType":null}}}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Type","description":null,"isOneOf":null,"fields":[{"name":"kind","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"__TypeKind","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"fields","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Field","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"interfaces","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"possibleTypes","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"enumValues","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__EnumValue","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"inputFields","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__InputValue","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"ofType","description":null,"args":[],"type":{"kind":"OBJECT","name":"__Type","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"specifiedBy","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"isOneOf","description":null,"args":[],"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"__TypeKind","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"ENUM","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INPUT_OBJECT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INTERFACE","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"LIST","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"NON_NULL","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"OBJECT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"SCALAR","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"UNION","description":null,"isDeprecated":false,"deprecationReason":null}],"possibleTypes":null}],"directives":[{"name":"skip","description":"The @skip directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional exclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"include","description":"The @include directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional inclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"specifiedBy","description":"The @specifiedBy directive is used within the type system definition language to provide a URL for specifying the behavior of custom scalar types. The URL should point to a human-readable specification of the data format, serialization, and coercion rules. It must not appear on built-in scalar types.","locations":["SCALAR"],"args":[{"name":"url","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}]}]}}""" ) } }, test("introspect schema with OneOf input objects") { val interpreter = graphQL(resolverOneOf).interpreter - interpreter.flatMap(_.execute(fullIntrospectionQuery)).map { response => + interpreter.flatMap(_.execute(fullIntrospectionQuery)).map(_.data.toString).map { response => assertTrue( - response.data.toString == - """{"__schema":{"queryType":{"name":"QueryWithOneOf"},"mutationType":null,"subscriptionType":null,"types":[{"kind":"SCALAR","name":"Boolean","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Captain","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"CaptainShipName","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"CaptainShipName","description":"Description of custom scalar emphasizing proper captain ship names","isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Character","description":null,"isOneOf":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"nicknames","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"origin","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"Origin","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"role","description":null,"args":[],"type":{"kind":"UNION","name":"Role","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"INPUT_OBJECT","name":"CharacterInput","description":null,"isOneOf":false,"fields":null,"inputFields":[{"name":"name","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null},{"name":"nicknames","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}}}},"defaultValue":null},{"name":"origin","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"Origin","ofType":null}},"defaultValue":null}],"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Engineer","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Float","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Int","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Mechanic","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"INPUT_OBJECT","name":"NameOrOriginInput","description":null,"isOneOf":true,"fields":null,"inputFields":[{"name":"name","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"origin","description":null,"type":{"kind":"ENUM","name":"Origin","ofType":null},"defaultValue":null}],"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"Origin","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"BELT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"EARTH","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MARS","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MOON","description":null,"isDeprecated":true,"deprecationReason":"Use: EARTH | MARS | BELT"}],"possibleTypes":null},{"kind":"OBJECT","name":"Pilot","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"QueryWithOneOf","description":"Queries","isOneOf":null,"fields":[{"name":"exists","description":null,"args":[{"name":"character","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"INPUT_OBJECT","name":"CharacterInput","ofType":null}},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"character","description":null,"args":[{"name":"nameOrOrigin","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"INPUT_OBJECT","name":"NameOrOriginInput","ofType":null}},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"Character","ofType":null}}}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"UNION","name":"Role","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":[{"kind":"OBJECT","name":"Captain","ofType":null},{"kind":"OBJECT","name":"Engineer","ofType":null},{"kind":"OBJECT","name":"Mechanic","ofType":null},{"kind":"OBJECT","name":"Pilot","ofType":null}]},{"kind":"SCALAR","name":"String","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null}],"directives":[{"name":"skip","description":"The @skip directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional exclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"include","description":"The @include directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional inclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"specifiedBy","description":"The @specifiedBy directive is used within the type system definition language to provide a URL for specifying the behavior of custom scalar types. The URL should point to a human-readable specification of the data format, serialization, and coercion rules. It must not appear on built-in scalar types.","locations":["SCALAR"],"args":[{"name":"url","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}]},{"name":"oneOf","description":"The `@oneOf` directive is used within the type system definition language to indicate an Input Object is a OneOf Input Object.","locations":["INPUT_OBJECT"],"args":[]}]}}""" + response == + """{"__schema":{"queryType":{"name":"QueryWithOneOf"},"mutationType":null,"subscriptionType":null,"types":[{"kind":"SCALAR","name":"Boolean","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Captain","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"CaptainShipName","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"CaptainShipName","description":"Description of custom scalar emphasizing proper captain ship names","isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Character","description":null,"isOneOf":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"nicknames","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"origin","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"Origin","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"role","description":null,"args":[],"type":{"kind":"UNION","name":"Role","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"INPUT_OBJECT","name":"CharacterInput","description":null,"isOneOf":false,"fields":null,"inputFields":[{"name":"name","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null},{"name":"nicknames","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}}}},"defaultValue":null},{"name":"origin","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"Origin","ofType":null}},"defaultValue":null}],"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Engineer","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Float","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Int","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Mechanic","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"INPUT_OBJECT","name":"NameOrOriginInput","description":null,"isOneOf":true,"fields":null,"inputFields":[{"name":"name","description":null,"type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"origin","description":null,"type":{"kind":"ENUM","name":"Origin","ofType":null},"defaultValue":null}],"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"Origin","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"BELT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"EARTH","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MARS","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MOON","description":null,"isDeprecated":true,"deprecationReason":"Use: EARTH | MARS | BELT"}],"possibleTypes":null},{"kind":"OBJECT","name":"Pilot","description":null,"isOneOf":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"QueryWithOneOf","description":"Queries","isOneOf":null,"fields":[{"name":"exists","description":null,"args":[{"name":"character","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"INPUT_OBJECT","name":"CharacterInput","ofType":null}},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"character","description":null,"args":[{"name":"nameOrOrigin","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"INPUT_OBJECT","name":"NameOrOriginInput","ofType":null}},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"Character","ofType":null}}}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"UNION","name":"Role","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":[{"kind":"OBJECT","name":"Captain","ofType":null},{"kind":"OBJECT","name":"Engineer","ofType":null},{"kind":"OBJECT","name":"Mechanic","ofType":null},{"kind":"OBJECT","name":"Pilot","ofType":null}]},{"kind":"SCALAR","name":"String","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Directive","description":null,"isOneOf":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"locations","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"__DirectiveLocation","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"args","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__InputValue","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"isRepeatable","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"__DirectiveLocation","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"ARGUMENT_DEFINITION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"ENUM","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"ENUM_VALUE","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"FIELD","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"FIELD_DEFINITION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"FRAGMENT_DEFINITION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"FRAGMENT_SPREAD","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INLINE_FRAGMENT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INPUT_FIELD_DEFINITION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INPUT_OBJECT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INTERFACE","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MUTATION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"OBJECT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"QUERY","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"SCALAR","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"SCHEMA","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"SUBSCRIPTION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"UNION","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"VARIABLE_DEFINITION","description":null,"isDeprecated":false,"deprecationReason":null}],"possibleTypes":null},{"kind":"OBJECT","name":"__EnumValue","description":null,"isOneOf":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"isDeprecated","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"deprecationReason","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Field","description":null,"isOneOf":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"args","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__InputValue","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"type","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"isDeprecated","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"deprecationReason","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__InputValue","description":null,"isOneOf":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"type","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"defaultValue","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"isDeprecated","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"deprecationReason","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Schema","description":null,"isOneOf":null,"fields":[{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"queryType","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"mutationType","description":null,"args":[],"type":{"kind":"OBJECT","name":"__Type","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"subscriptionType","description":null,"args":[],"type":{"kind":"OBJECT","name":"__Type","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"types","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"directives","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Directive","ofType":null}}}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Type","description":null,"isOneOf":null,"fields":[{"name":"kind","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"__TypeKind","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"fields","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Field","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"interfaces","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"possibleTypes","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"enumValues","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__EnumValue","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"inputFields","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__InputValue","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"ofType","description":null,"args":[],"type":{"kind":"OBJECT","name":"__Type","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"specifiedBy","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"isOneOf","description":null,"args":[],"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"__TypeKind","description":null,"isOneOf":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"ENUM","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INPUT_OBJECT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"INTERFACE","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"LIST","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"NON_NULL","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"OBJECT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"SCALAR","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"UNION","description":null,"isDeprecated":false,"deprecationReason":null}],"possibleTypes":null}],"directives":[{"name":"skip","description":"The @skip directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional exclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"include","description":"The @include directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional inclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"specifiedBy","description":"The @specifiedBy directive is used within the type system definition language to provide a URL for specifying the behavior of custom scalar types. The URL should point to a human-readable specification of the data format, serialization, and coercion rules. It must not appear on built-in scalar types.","locations":["SCALAR"],"args":[{"name":"url","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}]},{"name":"oneOf","description":"The `@oneOf` directive is used within the type system definition language to indicate an Input Object is a OneOf Input Object.","locations":["INPUT_OBJECT"],"args":[]}]}}""" ) } }, @@ -178,8 +178,8 @@ object IntrospectionSpec extends ZIOSpecDefault { } """) - interpreter.flatMap(_.execute(query)).map { response => - assertTrue(response.data.toString == """{"__type":{"name":"Captain"}}""") + interpreter.flatMap(_.execute(query)).map(_.data.toString).map { response => + assertTrue(response == """{"__type":{"name":"Captain"}}""") } }, test("introspect non-existent type") { @@ -192,8 +192,8 @@ object IntrospectionSpec extends ZIOSpecDefault { } """) - interpreter.flatMap(_.execute(query)).map { response => - assertTrue(response.data.toString == """{"__type":null}""") + interpreter.flatMap(_.execute(query)).map(_.data.toString).map { response => + assertTrue(response == """{"__type":null}""") } }, test("introspect the introspection itself") { @@ -214,9 +214,9 @@ object IntrospectionSpec extends ZIOSpecDefault { } """) - interpreter.flatMap(_.execute(query)).map { response => + interpreter.flatMap(_.execute(query)).map(_.data.toString).map { response => assertTrue( - response.data.toString == """{"__type":{"name":"__Schema","fields":[{"name":"description"},{"name":"queryType"},{"name":"mutationType"},{"name":"subscriptionType"},{"name":"types"},{"name":"directives"}]},"__schema":{"types":[{"name":"Boolean"},{"name":"Captain"},{"name":"CaptainShipName"},{"name":"Character"},{"name":"Engineer"},{"name":"Float"},{"name":"Int"},{"name":"Mechanic"},{"name":"Origin"},{"name":"Pilot"},{"name":"QueryIO"},{"name":"Role"},{"name":"String"},{"name":"__Directive"},{"name":"__DirectiveLocation"},{"name":"__EnumValue"},{"name":"__Field"},{"name":"__InputValue"},{"name":"__Schema"},{"name":"__Type"},{"name":"__TypeKind"}]}}""" + response == """{"__type":{"name":"__Schema","fields":[{"name":"description"},{"name":"queryType"},{"name":"mutationType"},{"name":"subscriptionType"},{"name":"types"},{"name":"directives"}]},"__schema":{"types":[{"name":"Boolean"},{"name":"Captain"},{"name":"CaptainShipName"},{"name":"Character"},{"name":"Engineer"},{"name":"Float"},{"name":"Int"},{"name":"Mechanic"},{"name":"Origin"},{"name":"Pilot"},{"name":"QueryIO"},{"name":"Role"},{"name":"String"},{"name":"__Directive"},{"name":"__DirectiveLocation"},{"name":"__EnumValue"},{"name":"__Field"},{"name":"__InputValue"},{"name":"__Schema"},{"name":"__Type"},{"name":"__TypeKind"}]}}""" ) } }, From 56fa54f6087bc540a87ae93ed0e66325b56c6581 Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Wed, 7 Feb 2024 08:52:50 +1100 Subject: [PATCH 18/25] Disable mima --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 4d908098ac..72756e42c9 100644 --- a/build.sbt +++ b/build.sbt @@ -711,7 +711,7 @@ lazy val commonSettings = Def.settings( }) ) -lazy val enforceMimaCompatibility = true // Enable / disable failing CI on binary incompatibilities +lazy val enforceMimaCompatibility = false // Enable / disable failing CI on binary incompatibilities lazy val enableMimaSettingsJVM = Def.settings( From cd926b3f43df729bee52d88a634d713358717bcb Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Mon, 3 Jun 2024 10:28:12 +0100 Subject: [PATCH 19/25] Reuse `hasAnnotation` macro --- .../caliban/schema/ArgBuilderDerivation.scala | 4 ++-- .../main/scala-3/caliban/schema/macros/Macros.scala | 13 ++++--------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala b/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala index 13a0845693..e72bf04f67 100644 --- a/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala +++ b/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala @@ -2,7 +2,7 @@ package caliban.schema import caliban.CalibanError.ExecutionError import caliban.Value.* -import caliban.schema.Annotations.{ GQLDefault, GQLName } +import caliban.schema.Annotations.{ GQLDefault, GQLName, GQLOneOfInput } import caliban.schema.macros.Macros import caliban.{ CalibanError, InputValue } import magnolia1.Macro as MagnoliaMacro @@ -53,7 +53,7 @@ trait CommonArgBuilderDerivation { inline def derived[A]: ArgBuilder[A] = inline summonInline[Mirror.Of[A]] match { case m: Mirror.SumOf[A] => - inline if (Macros.hasOneOfInputAnnotation[A]) { + inline if (Macros.hasAnnotation[A, GQLOneOfInput]) { makeOneOfBuilder[A]( recurseSum[A, m.MirroredElemLabels, m.MirroredElemTypes](), constValue[m.MirroredLabel] diff --git a/core/src/main/scala-3/caliban/schema/macros/Macros.scala b/core/src/main/scala-3/caliban/schema/macros/Macros.scala index 1ec76902f2..89497869cc 100644 --- a/core/src/main/scala-3/caliban/schema/macros/Macros.scala +++ b/core/src/main/scala-3/caliban/schema/macros/Macros.scala @@ -8,11 +8,10 @@ import scala.quoted.* export magnolia1.TypeInfo object Macros { - inline def isFieldExcluded[P, T]: Boolean = ${ isFieldExcludedImpl[P, T] } - inline def isEnumField[P, T]: Boolean = ${ isEnumFieldImpl[P, T] } - inline def implicitExists[T]: Boolean = ${ implicitExistsImpl[T] } - inline def hasAnnotation[T, Ann]: Boolean = ${ hasAnnotationImpl[T, Ann] } - inline def hasOneOfInputAnnotation[P]: Boolean = ${ hasOneOfInputAnnotationImpl[P] } + inline def isFieldExcluded[P, T]: Boolean = ${ isFieldExcludedImpl[P, T] } + inline def isEnumField[P, T]: Boolean = ${ isEnumFieldImpl[P, T] } + inline def implicitExists[T]: Boolean = ${ implicitExistsImpl[T] } + inline def hasAnnotation[T, Ann]: Boolean = ${ hasAnnotationImpl[T, Ann] } inline def fieldsFromMethods[R, T]: List[(String, List[Any], Schema[R, ?])] = ${ fieldsFromMethodsImpl[R, T] } @@ -115,8 +114,4 @@ If you use a custom type as an argument, you also need to provide an implicit Ar See https://ghostdogpr.github.io/caliban/docs/schema.html for more information. """ - def hasOneOfInputAnnotationImpl[T: Type](using q: Quotes): Expr[Boolean] = { - import q.reflect.* - Expr(TypeRepr.of[T].typeSymbol.annotations.exists(_.tpe.typeSymbol.name == "GQLOneOfInput")) - } } From 2611c5b3b95a13f084250ff8fc7c83d9d45bcff3 Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Mon, 3 Jun 2024 11:30:29 +0100 Subject: [PATCH 20/25] Change `parentTypeName` to `parentType` on `__InputValue` --- .../caliban/schema/SchemaDerivation.scala | 5 ++- .../caliban/schema/DerivationUtils.scala | 44 ++++++++++--------- .../introspection/adt/__InputValue.scala | 4 +- .../caliban/transformers/Transformer.scala | 8 ++-- .../scala/caliban/validation/Validator.scala | 5 ++- .../validation/ValidationSchemaSpec.scala | 2 +- 6 files changed, 37 insertions(+), 31 deletions(-) diff --git a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala index a0de15727e..6e29f3eef4 100644 --- a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala @@ -67,7 +67,7 @@ trait CommonSchemaDerivation[R] { if (isScalarValueType(ctx)) makeScalar(getName(ctx), getDescription(ctx)) else ctx.parameters.head.typeclass.toType_(isInput, isSubscription) } else if (isInput) { - makeInputObject( + lazy val tpe: __Type = makeInputObject( Some(ctx.annotations.collectFirst { case GQLInputName(suffix) => suffix } .getOrElse(customizeInputTypeName(getName(ctx)))), getDescription(ctx), @@ -83,13 +83,14 @@ trait CommonSchemaDerivation[R] { p.annotations.collectFirst { case GQLDeprecated(_) => () }.isDefined, p.annotations.collectFirst { case GQLDeprecated(reason) => reason }, Some(p.annotations.collect { case GQLDirective(dir) => dir }.toList).filter(_.nonEmpty), - Some(ctx.typeName.short) + () => Some(tpe) ) ) .toList, Some(ctx.typeName.full), Some(getDirectives(ctx)) ) + tpe } else makeObject( Some(getName(ctx)), diff --git a/core/src/main/scala-3/caliban/schema/DerivationUtils.scala b/core/src/main/scala-3/caliban/schema/DerivationUtils.scala index b9c163f8b6..9070b839c1 100644 --- a/core/src/main/scala-3/caliban/schema/DerivationUtils.scala +++ b/core/src/main/scala-3/caliban/schema/DerivationUtils.scala @@ -97,26 +97,30 @@ private object DerivationUtils { annotations: List[Any], fields: List[(String, List[Any], Schema[R, Any])], info: TypeInfo - )(isInput: Boolean, isSubscription: Boolean): __Type = makeInputObject( - Some(getInputName(annotations).getOrElse(customizeInputTypeName(getName(annotations, info)))), - getDescription(annotations), - fields.map { (name, fieldAnnotations, schema) => - __InputValue( - name, - getDescription(fieldAnnotations), - () => - if (schema.optional) schema.toType_(isInput, isSubscription) - else schema.toType_(isInput, isSubscription).nonNull, - getDefaultValue(fieldAnnotations), - getDeprecatedReason(fieldAnnotations).isDefined, - getDeprecatedReason(fieldAnnotations), - Some(getDirectives(fieldAnnotations)).filter(_.nonEmpty), - Some(info.short) - ) - }, - Some(info.full), - Some(getDirectives(annotations)) - ) + )(isInput: Boolean, isSubscription: Boolean): __Type = { + lazy val tpe: __Type = makeInputObject( + Some(getInputName(annotations).getOrElse(customizeInputTypeName(getName(annotations, info)))), + getDescription(annotations), + fields.map { (name, fieldAnnotations, schema) => + val deprecationReason = getDeprecatedReason(fieldAnnotations) + __InputValue( + name, + description = getDescription(fieldAnnotations), + `type` = () => + if (schema.optional) schema.toType_(isInput, isSubscription) + else schema.toType_(isInput, isSubscription).nonNull, + defaultValue = getDefaultValue(fieldAnnotations), + isDeprecated = deprecationReason.isDefined, + deprecationReason = deprecationReason, + directives = Some(getDirectives(fieldAnnotations)).filter(_.nonEmpty), + parentType = () => Some(tpe) + ) + }, + Some(info.full), + Some(getDirectives(annotations)) + ) + tpe + } def mkOneOfInput[R]( annotations: List[Any], diff --git a/core/src/main/scala/caliban/introspection/adt/__InputValue.scala b/core/src/main/scala/caliban/introspection/adt/__InputValue.scala index 0242314377..3ab5e06124 100644 --- a/core/src/main/scala/caliban/introspection/adt/__InputValue.scala +++ b/core/src/main/scala/caliban/introspection/adt/__InputValue.scala @@ -5,7 +5,6 @@ import caliban.parsing.adt.Definition.TypeSystemDefinition.TypeDefinition.InputV import caliban.parsing.adt.Directive import caliban.parsing.Parser import caliban.schema.Annotations.GQLExcluded -import caliban.schema.Annotations.GQLExcluded import scala.annotation.tailrec @@ -17,7 +16,7 @@ case class __InputValue( isDeprecated: Boolean = false, deprecationReason: Option[String] = None, @GQLExcluded directives: Option[List[Directive]] = None, - @GQLExcluded parentTypeName: Option[String] = None + @GQLExcluded parentType: () => Option[__Type] = () => None ) { def toInputValueDefinition: InputValueDefinition = { val default = defaultValue.flatMap(v => Parser.parseInputValue(v).toOption) @@ -33,6 +32,7 @@ case class __InputValue( } private[caliban] lazy val _type: __Type = `type`() + private[caliban] lazy val _parentType = parentType() /** * Makes the [[`type`]] nullable as required by the spec for OneOf Input Objects diff --git a/core/src/main/scala/caliban/transformers/Transformer.scala b/core/src/main/scala/caliban/transformers/Transformer.scala index 11a157f491..7c7ca5f677 100644 --- a/core/src/main/scala/caliban/transformers/Transformer.scala +++ b/core/src/main/scala/caliban/transformers/Transformer.scala @@ -246,18 +246,18 @@ object Transformer { val typeVisitor: TypeVisitor = TypeVisitor.fields.modify { field => - def loop(parentType: Option[String])(arg: __InputValue): Option[__InputValue] = - parentType.flatMap(map.get) match { + def loop(arg: __InputValue): Option[__InputValue] = + arg._parentType.flatMap(_.name).flatMap(map.get) match { case Some(s) if arg._type.isNullable && s.contains(arg.name) => None case _ => lazy val newType = arg._type.mapInnerType { t => - t.copy(inputFields = t.inputFields(_).map(_.flatMap(loop(t.name)))) + t.copy(inputFields = t.inputFields(_).map(_.flatMap(loop))) } Some(arg.copy(`type` = () => newType)) } - field.copy(args = field.args(_).flatMap(loop(None))) + field.copy(args = field.args(_).flatMap(loop)) } protected val typeNames: Set[String] = Set.empty diff --git a/core/src/main/scala/caliban/validation/Validator.scala b/core/src/main/scala/caliban/validation/Validator.scala index e32beec54d..9ca624920c 100644 --- a/core/src/main/scala/caliban/validation/Validator.scala +++ b/core/src/main/scala/caliban/validation/Validator.scala @@ -933,9 +933,10 @@ object Validator { } def noDuplicatedOneOfOrigin(inputValues: List[__InputValue]): EReader[Any, ValidationError, Unit] = { - val resolveOrigin = (i: __InputValue) => i.parentTypeName.getOrElse("") + val resolveOrigin = (i: __InputValue) => + i._parentType.flatMap(_.origin).getOrElse("") val messageBuilder = (i: __InputValue) => - s"$inputObjectContext has multiple arguments from the same case class: ${resolveOrigin(i)}" + s"$inputObjectContext is extended by a case class with multiple arguments: ${resolveOrigin(i)}" val explanatory = "All case classes used as arguments to OneOf Input Objects must have exactly one field" noDuplicateName[__InputValue](inputValues, resolveOrigin, messageBuilder, explanatory) } diff --git a/core/src/test/scala/caliban/validation/ValidationSchemaSpec.scala b/core/src/test/scala/caliban/validation/ValidationSchemaSpec.scala index 8f5573afe2..cabbe2c5d3 100644 --- a/core/src/test/scala/caliban/validation/ValidationSchemaSpec.scala +++ b/core/src/test/scala/caliban/validation/ValidationSchemaSpec.scala @@ -363,7 +363,7 @@ object ValidationSchemaSpec extends ZIOSpecDefault { check( graphQL(RootResolver(Queries(_.toString))), - "OneOf InputObject 'FooInput' has multiple arguments from the same case class: ArgA" + "OneOf InputObject 'FooInput' is extended by a case class with multiple arguments: caliban.validation.ValidationSchemaSpec.spec.ArgA" ) }, test("cannot have default values") { From 2984b44f0770f47d7507911ff937361ec5c6b2f1 Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Mon, 3 Jun 2024 12:07:03 +0100 Subject: [PATCH 21/25] Remove `isOneOf` argument from `makeInputObject` --- .../src/main/scala-2/caliban/schema/SchemaDerivation.scala | 5 ++--- core/src/main/scala-3/caliban/schema/DerivationUtils.scala | 5 ++--- core/src/main/scala/caliban/parsing/adt/Directive.scala | 7 ++++++- core/src/main/scala/caliban/schema/Schema.scala | 3 +-- core/src/main/scala/caliban/schema/Types.scala | 7 +++---- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala index 6e29f3eef4..40bbd51d48 100644 --- a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala @@ -3,7 +3,7 @@ package caliban.schema import caliban.CalibanError.ValidationError import caliban.Value._ import caliban.introspection.adt._ -import caliban.parsing.adt.Directive +import caliban.parsing.adt.{ Directive, Directives } import caliban.schema.Annotations._ import caliban.schema.Types._ import magnolia1._ @@ -202,8 +202,7 @@ trait CommonSchemaDerivation[R] { p.typeclass.toType_(isInput = true).allInputFields.map(_.nullable) }, Some(ctx.typeName.full), - Some(List(Directive("oneOf"))), - isOneOf = true + Some(List(Directive(Directives.OneOf))) ) } else if (!isInterface) { containsEmptyUnionObjects = emptyUnionObjectIdxs.contains(true) diff --git a/core/src/main/scala-3/caliban/schema/DerivationUtils.scala b/core/src/main/scala-3/caliban/schema/DerivationUtils.scala index 9070b839c1..d8f4a0d2bf 100644 --- a/core/src/main/scala-3/caliban/schema/DerivationUtils.scala +++ b/core/src/main/scala-3/caliban/schema/DerivationUtils.scala @@ -1,7 +1,7 @@ package caliban.schema import caliban.introspection.adt.* -import caliban.parsing.adt.Directive +import caliban.parsing.adt.{ Directive, Directives } import caliban.schema.Annotations.* import caliban.schema.Types.* import magnolia1.TypeInfo @@ -132,8 +132,7 @@ private object DerivationUtils { getDescription(annotations), schemas.flatMap(_.toType_(isInput = true).allInputFields.map(_.nullable)), Some(info.full), - Some(List(Directive("oneOf"))), - isOneOf = true + Some(List(Directive(Directives.OneOf))) ) def mkObject[R]( diff --git a/core/src/main/scala/caliban/parsing/adt/Directive.scala b/core/src/main/scala/caliban/parsing/adt/Directive.scala index 4d862853bd..e3cc12df1f 100644 --- a/core/src/main/scala/caliban/parsing/adt/Directive.scala +++ b/core/src/main/scala/caliban/parsing/adt/Directive.scala @@ -9,6 +9,7 @@ object Directives { final val LazyDirective = "lazy" final val NewtypeDirective = "newtype" final val DeprecatedDirective = "deprecated" + final val OneOf = "oneOf" def isDeprecated(directives: List[Directive]): Boolean = directives.exists(_.name == DeprecatedDirective) @@ -16,11 +17,15 @@ object Directives { def deprecationReason(directives: List[Directive]): Option[String] = findDirective(directives, DeprecatedDirective, "reason") - def isNewType(directives: List[Directive]): Boolean = + def isNewType(directives: List[Directive]): Boolean = directives.exists(_.name == NewtypeDirective) + def newTypeName(directives: List[Directive]): Option[String] = findDirective(directives, NewtypeDirective, "name") + def isOneOf(directives: List[Directive]): Boolean = + directives.exists(_.name == OneOf) + private def findDirective(directives: List[Directive], name: String, argument: String): Option[String] = directives.collectFirst { case f if f.name == name => diff --git a/core/src/main/scala/caliban/schema/Schema.scala b/core/src/main/scala/caliban/schema/Schema.scala index d5298ab2b3..56b8e57c06 100644 --- a/core/src/main/scala/caliban/schema/Schema.scala +++ b/core/src/main/scala/caliban/schema/Schema.scala @@ -203,8 +203,7 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { fields(isInput, isSubscription).map { case (f, _) => __InputValue(f.name, f.description, f.`type`, None, directives = f.directives) }, - directives = Some(directives), - isOneOf = directives.exists(_.name == "oneOf") + directives = Some(directives) ) } else makeObject(Some(name), description, fields(isInput, isSubscription).map(_._1), directives) } diff --git a/core/src/main/scala/caliban/schema/Types.scala b/core/src/main/scala/caliban/schema/Types.scala index 873765fb93..3ddf7eaf9c 100644 --- a/core/src/main/scala/caliban/schema/Types.scala +++ b/core/src/main/scala/caliban/schema/Types.scala @@ -1,7 +1,7 @@ package caliban.schema import caliban.introspection.adt._ -import caliban.parsing.adt.Directive +import caliban.parsing.adt.{ Directive, Directives } import scala.annotation.tailrec @@ -87,8 +87,7 @@ object Types { description: Option[String], fields: List[__InputValue], origin: Option[String] = None, - directives: Option[List[Directive]] = None, - isOneOf: Boolean = false + directives: Option[List[Directive]] = None ): __Type = __Type( __TypeKind.INPUT_OBJECT, @@ -99,7 +98,7 @@ object Types { else Some(fields.filter(!_.isDeprecated)), origin = origin, directives = directives, - isOneOf = Some(isOneOf) + isOneOf = Some(Directives.isOneOf(directives.getOrElse(Nil))) ) def makeUnion( From 9120cc7a0acb9f6a0a2e42b05800fd4a62285ae3 Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Mon, 3 Jun 2024 12:17:52 +0100 Subject: [PATCH 22/25] Fix mima --- build.sbt | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 7b945d2ca3..830c72ad5b 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,10 @@ -import com.typesafe.tools.mima.core.{ DirectMissingMethodProblem, MissingClassProblem, ProblemFilters } +import com.typesafe.tools.mima.core.{ + DirectMissingMethodProblem, + FinalMethodProblem, + MissingClassProblem, + MissingTypesProblem, + ProblemFilters +} import org.scalajs.linker.interface.ModuleSplitStyle import sbtcrossproject.CrossPlugin.autoImport.{ crossProject, CrossType } @@ -774,7 +780,14 @@ lazy val enableMimaSettingsJVM = Def.settings( mimaFailOnProblem := enforceMimaCompatibility, mimaPreviousArtifacts := previousStableVersion.value.map(organization.value %% moduleName.value % _).toSet, - mimaBinaryIssueFilters ++= Seq() + mimaBinaryIssueFilters ++= Seq( + ProblemFilters.exclude[DirectMissingMethodProblem]("caliban.parsing.adt.Type.$init$"), + ProblemFilters.exclude[DirectMissingMethodProblem]("caliban.introspection.adt.__Type.*"), + ProblemFilters.exclude[DirectMissingMethodProblem]("caliban.introspection.adt.__InputValue.*"), + ProblemFilters.exclude[FinalMethodProblem]("caliban.parsing.adt.Type*"), + ProblemFilters.exclude[MissingTypesProblem]("caliban.introspection.adt.__Type$"), + ProblemFilters.exclude[MissingTypesProblem]("caliban.introspection.adt.__InputValue$") + ) ) lazy val enableMimaSettingsJS = From 2c542b0b6d6148e69edfde045a9d778c03d55b63 Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Mon, 3 Jun 2024 12:20:33 +0100 Subject: [PATCH 23/25] Micro-optimize validation --- core/src/main/scala/caliban/validation/Validator.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/caliban/validation/Validator.scala b/core/src/main/scala/caliban/validation/Validator.scala index 9ca624920c..1f9a041a76 100644 --- a/core/src/main/scala/caliban/validation/Validator.scala +++ b/core/src/main/scala/caliban/validation/Validator.scala @@ -959,12 +959,13 @@ object Validator { } t.allInputFields match { - case Nil => + case Nil => failValidation( s"$inputObjectContext does not have fields", "An Input Object type must define one or more input fields" ) - case fields => ZPure.when(t._isOneOfInput)(validateOneOfFields(fields)) *> validateFields(fields) + case fields if t._isOneOfInput => validateOneOfFields(fields) *> validateFields(fields) + case fields => validateFields(fields) } } From 4f7c3e709ace80ba6f227e10d77cf240eadca150 Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Tue, 4 Jun 2024 10:33:59 +0100 Subject: [PATCH 24/25] Reimplement handling of OneOf inputs via a PartialFunction --- .../caliban/schema/ArgBuilderDerivation.scala | 62 ++++++++++------ .../caliban/schema/ArgBuilderDerivation.scala | 73 +++++++++++-------- .../scala/caliban/schema/ArgBuilder.scala | 14 ++++ 3 files changed, 97 insertions(+), 52 deletions(-) diff --git a/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala b/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala index 5a43c54ee9..5fdf3b6d86 100644 --- a/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala @@ -24,17 +24,34 @@ trait CommonArgBuilderDerivation { override def map[A, B](from: EitherExecutionError[A])(fn: A => B): EitherExecutionError[B] = from.map(fn) } - def join[T](ctx: CaseClass[ArgBuilder, T]): ArgBuilder[T] = - (input: InputValue) => + def join[T](ctx: CaseClass[ArgBuilder, T]): ArgBuilder[T] = new ArgBuilder[T] { + + private val params = Array.from(ctx.parameters.map { p => + val label = p.annotations.collectFirst { case GQLName(name) => name }.getOrElse(p.label) + val default = p.typeclass.buildMissing(p.annotations.collectFirst { case GQLDefault(v) => v }) + (label, default) + }) + + private val required = params.collect { case (label, default) if default.isLeft => label } + + 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] = + input match { + case InputValue.ObjectValue(fields) => fromFields(fields) + case _ => Left(ExecutionError("expected an input object")) + } + + private[this] def fromFields(fields: Map[String, InputValue]): Either[ExecutionError, T] = ctx.constructMonadic { p => - input match { - case InputValue.ObjectValue(fields) => - val label = p.annotations.collectFirst { case GQLName(name) => name }.getOrElse(p.label) - val default = p.annotations.collectFirst { case GQLDefault(v) => v } - fields.get(label).fold(p.typeclass.buildMissing(default))(p.typeclass.build) - case value => p.typeclass.build(value) - } + val idx = p.index + val (label, default) = params(idx) + val field = fields.getOrElse(label, null) + if (field ne null) p.typeclass.build(field) else default } + } def split[T](ctx: SealedTrait[ArgBuilder, T]): ArgBuilder[T] = if (ctx.annotations.contains(GQLOneOfInput())) makeOneOfBuilder(ctx) @@ -57,26 +74,25 @@ trait CommonArgBuilderDerivation { case None => Left(ExecutionError(s"Can't build a trait from input $input")) } - private def makeOneOfBuilder[A](ctx: SealedTrait[ArgBuilder, A]): ArgBuilder[A] = - new ArgBuilder[A] { + 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}") + private def inputError(input: InputValue) = + ExecutionError(s"Invalid oneOf input $input for trait ${ctx.typeName.short}") - private val combined = ctx.subtypes.map(_.typeclass).toList.asInstanceOf[List[ArgBuilder[A]]] match { - case head :: tail => - tail.foldLeft(head)(_ orElse _).orElse(input => Left(inputError(input))) - case _ => - (_ => Left(ExecutionError("OneOf Input Objects must have at least one subtype"))): ArgBuilder[A] - } + override val partial: PartialFunction[InputValue, Either[ExecutionError, A]] = { + val xs = ctx.subtypes.map(_.typeclass).toList.asInstanceOf[List[ArgBuilder[A]]] - def build(input: InputValue): Either[ExecutionError, A] = input match { - case InputValue.ObjectValue(f) if f.size == 1 => combined.build(input) - case InputValue.ObjectValue(_) => Left(ExecutionError("Exactly one key must be specified for oneOf inputs")) - case _ => Left(inputError(input)) + 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) } + def build(input: InputValue): Either[ExecutionError, A] = + partial.applyOrElse(input, (in: InputValue) => Left(inputError(in))) + } + } trait ArgBuilderDerivation extends CommonArgBuilderDerivation { diff --git a/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala b/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala index e72bf04f67..3ddafa2a2e 100644 --- a/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala +++ b/core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala @@ -78,7 +78,7 @@ trait CommonArgBuilderDerivation { private lazy val subTypes = _subTypes private val emptyInput = InputValue.ObjectValue(Map.empty) - def build(input: InputValue): Either[ExecutionError, A] = + final def build(input: InputValue): Either[ExecutionError, A] = input.match { case EnumValue(value) => Right(value) case StringValue(value) => Right(value) @@ -99,52 +99,67 @@ trait CommonArgBuilderDerivation { private def makeOneOfBuilder[A]( _subTypes: => List[(String, List[Any], ArgBuilder[Any])], - _traitLabel: => String + traitLabel: String ): ArgBuilder[A] = new ArgBuilder[A] { - private lazy val traitLabel = _traitLabel - - private lazy val combined: ArgBuilder[A] = - _subTypes.map(_._3).asInstanceOf[List[ArgBuilder[A]]] match { - case head :: tail => - tail.foldLeft(head)(_ orElse _).orElse((input: InputValue) => Left(inputError(input))) - case _ => - (_: InputValue) => Left(ExecutionError("OneOf Input Objects must have at least one subtype")) + + 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) + } + + def build(input: InputValue): Either[ExecutionError, A] = + partial.applyOrElse(input, (in: InputValue) => Left(inputError(in))) private def inputError(input: InputValue) = ExecutionError(s"Invalid oneOf input $input for trait $traitLabel") - - def build(input: InputValue): Either[ExecutionError, A] = input match { - case InputValue.ObjectValue(f) if f.size == 1 => combined.build(input) - case InputValue.ObjectValue(_) => Left(ExecutionError("Exactly one key must be specified for oneOf inputs")) - case _ => Left(inputError(input)) - } } private def makeProductArgBuilder[A]( _fields: => List[(String, ArgBuilder[Any])], annotations: Map[String, List[Any]] - )(fromProduct: Product => A) = new ArgBuilder[A] { + )(fromProduct: Product => A): ArgBuilder[A] = new ArgBuilder[A] { - private lazy val fields = _fields.map { (label, builder) => + private val params = Array.from(_fields.map { (label, builder) => val labelList = annotations.get(label) - val default = labelList.flatMap(_.collectFirst { case GQLDefault(v) => v }) + val default = builder.buildMissing(labelList.flatMap(_.collectFirst { case GQLDefault(v) => v })) val finalLabel = labelList.flatMap(_.collectFirst { case GQLName(name) => name }).getOrElse(label) (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) } def build(input: InputValue): Either[ExecutionError, A] = - fields.view.map { (label, default, builder) => - input match { - case InputValue.ObjectValue(fields) => fields.get(label).fold(builder.buildMissing(default))(builder.build) - case value => builder.build(value) - } - }.foldLeft[Either[ExecutionError, Tuple]](Right(EmptyTuple)) { (acc, item) => - item match { - case Right(value) => acc.map(_ :* value) - case Left(e) => Left(e) + input match { + case InputValue.ObjectValue(fields) => fromFields(fields) + case _ => Left(ExecutionError("expected an input object")) + } + + private def fromFields(fields: Map[String, InputValue]): Either[ExecutionError, A] = { + var i = 0 + val l = params.length + var acc: Tuple = EmptyTuple + while (i < l) { + val (label, default, builder) = params(i) + val field = fields.getOrElse(label, null) + val value = if (field ne null) builder.build(field) else default + value match { + case Right(v) => acc :*= v + case e @ Left(_) => return e.asInstanceOf[Either[ExecutionError, A]] } - }.map(fromProduct) + i += 1 + } + Right(fromProduct(acc)) + } + } } diff --git a/core/src/main/scala/caliban/schema/ArgBuilder.scala b/core/src/main/scala/caliban/schema/ArgBuilder.scala index 78ce9157e5..985f97d213 100644 --- a/core/src/main/scala/caliban/schema/ArgBuilder.scala +++ b/core/src/main/scala/caliban/schema/ArgBuilder.scala @@ -35,6 +35,20 @@ trait ArgBuilder[T] { self => */ 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. From c327df792faa60d78950054285105214a625e0d8 Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Tue, 4 Jun 2024 10:43:01 +0100 Subject: [PATCH 25/25] Fix Scala 2.12 --- .../caliban/schema/ArgBuilderDerivation.scala | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala b/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala index 5fdf3b6d86..be5d5ae816 100644 --- a/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala @@ -6,6 +6,7 @@ import caliban.Value._ import caliban.schema.Annotations.{ GQLDefault, GQLName, GQLOneOfInput } import magnolia1._ +import scala.collection.compat._ import scala.language.experimental.macros trait CommonArgBuilderDerivation { @@ -26,11 +27,15 @@ trait CommonArgBuilderDerivation { def join[T](ctx: CaseClass[ArgBuilder, T]): ArgBuilder[T] = new ArgBuilder[T] { - private val params = Array.from(ctx.parameters.map { p => - val label = p.annotations.collectFirst { case GQLName(name) => name }.getOrElse(p.label) - val default = p.typeclass.buildMissing(p.annotations.collectFirst { case GQLDefault(v) => v }) - (label, default) - }) + private val params = { + val arr = Array.ofDim[(String, EitherExecutionError[Any])](ctx.parameters.length) + ctx.parameters.zipWithIndex.foreach { case (p, i) => + val label = p.annotations.collectFirst { case GQLName(name) => name }.getOrElse(p.label) + val default = p.typeclass.buildMissing(p.annotations.collectFirst { case GQLDefault(v) => v }) + arr(i) = (label, default) + } + arr + } private val required = params.collect { case (label, default) if default.isLeft => label }