From af99989deb4b72624ccd49f462b04f2323ecb71e Mon Sep 17 00:00:00 2001 From: Dan Di Spaltro Date: Mon, 27 Aug 2018 11:52:54 -0700 Subject: [PATCH 1/2] Add a MappedAbstractType that will transform the input value - Since Scala 2.x does not have union types, one can represent them a number of different ways, a common way is to use a boxed type like Either, or Coproduct from various libraries. To use these boxed types is impossible in Sangria because it basically expects the type to unboxed, and inheriting from `Any` is the only way to achieve that. - This introduces a concept to map a type `T` to Any such that you can unbox after the definition, and get a value what the native type defs expect. --- .../scala/sangria/execution/Resolver.scala | 11 ++++ src/main/scala/sangria/schema/Schema.scala | 9 +++- .../execution/UnionInterfaceSpec.scala | 51 ++++++++++++++++++- 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/main/scala/sangria/execution/Resolver.scala b/src/main/scala/sangria/execution/Resolver.scala index de6a7e8b..6baa15c2 100644 --- a/src/main/scala/sangria/execution/Resolver.scala +++ b/src/main/scala/sangria/execution/Resolver.scala @@ -901,6 +901,17 @@ class Resolver[Ctx]( resolveActionsPar(path, obj, actions, userCtx, fields.namesOrdered) case Failure(error) ⇒ Result(ErrorRegistry(path, error), None) } + case abst: MappedAbstractType[Any @unchecked] ⇒ + val newValue = abst.contraMap(value) + if (isUndefinedValue(value)) + Result(ErrorRegistry.empty, None) + else + abst.typeOf(newValue, schema) match { + case Some(obj) ⇒ resolveValue(path, astFields, obj, field, newValue, userCtx) + case None ⇒ Result(ErrorRegistry(path, + UndefinedConcreteTypeError(path, abst, schema.possibleTypes.getOrElse(abst.name, Vector.empty), newValue, exceptionHandler, sourceMapper, astFields.head.location.toList)), None) + } + case abst: AbstractType ⇒ if (isUndefinedValue(value)) Result(ErrorRegistry.empty, None) diff --git a/src/main/scala/sangria/schema/Schema.scala b/src/main/scala/sangria/schema/Schema.scala index 710d15e0..bf94bd65 100644 --- a/src/main/scala/sangria/schema/Schema.scala +++ b/src/main/scala/sangria/schema/Schema.scala @@ -65,6 +65,10 @@ sealed trait AbstractType extends Type with Named { schema.possibleTypes get name flatMap (_.find(_ isInstanceOf value).asInstanceOf[Option[ObjectType[Ctx, _]]]) } +sealed trait MappedAbstractType[T] extends Type with AbstractType with OutputType[T] { + def contraMap(value: T): Any +} + sealed trait NullableType sealed trait UnmodifiedType @@ -295,6 +299,9 @@ case class UnionType[Ctx]( astNodes: Vector[ast.AstNode] = Vector.empty) extends OutputType[Any] with CompositeType[Any] with AbstractType with NullableType with UnmodifiedType with HasAstInfo { def rename(newName: String) = copy(name = newName).asInstanceOf[this.type] def toAst: ast.TypeDefinition = SchemaRenderer.renderType(this) + def map[T](func: T => Any): OutputType[T] with MappedAbstractType[T] = new UnionType[Ctx](name, description, types, astDirectives, astNodes) with MappedAbstractType[T] { + override def contraMap(value: T): Any = func(value) + }.asInstanceOf[OutputType[T] with MappedAbstractType[T]] } case class Field[Ctx, Val]( @@ -760,7 +767,7 @@ case class Schema[Ctx, Val]( def renderPretty: String = toAst.renderPretty def renderPretty(filter: SchemaFilter): String = toAst(filter).renderPretty - + def renderCompact: String = toAst.renderCompact def renderCompact(filter: SchemaFilter): String = toAst(filter).renderCompact diff --git a/src/test/scala/sangria/execution/UnionInterfaceSpec.scala b/src/test/scala/sangria/execution/UnionInterfaceSpec.scala index a5bf5a52..5fd0c8fd 100644 --- a/src/test/scala/sangria/execution/UnionInterfaceSpec.scala +++ b/src/test/scala/sangria/execution/UnionInterfaceSpec.scala @@ -13,7 +13,12 @@ class UnionInterfaceSpec extends WordSpec with Matchers with FutureResultSupport case class Dog(name: Option[String], barks: Option[Boolean]) extends Named case class Cat(name: Option[String], meows: Option[Boolean]) extends Named - case class Person(name: Option[String], pets: Option[List[Option[Any]]], friends: Option[List[Option[Named]]]) extends Named + case class Person(name: Option[String], pets: Option[List[Option[Any]]], friends: Option[List[Option[Named]]]) extends Named { + def eitherPets: Option[List[Option[Either[Dog, Cat]]]] = pets.map(_.map(_.map { + case d: Dog => Left(d) + case c: Cat => Right(c) + })) + } val NamedType = InterfaceType("Named", fields[Unit, Named]( Field("name", OptionType(StringType), resolve = _.value.name))) @@ -26,8 +31,11 @@ class UnionInterfaceSpec extends WordSpec with Matchers with FutureResultSupport val PetType = UnionType[Unit]("Pet", types = DogType :: CatType :: Nil) + val Pet2Type = UnionType[Unit]("Pet2", types = DogType :: CatType :: Nil).map[Either[Dog, Cat]](_.fold(dog => dog: Any, cat => cat: Any)) + val PersonType = ObjectType("Person", interfaces[Unit, Person](NamedType), fields[Unit, Person]( Field("pets", OptionType(ListType(OptionType(PetType))), resolve = _.value.pets), + Field("pets2", OptionType(ListType(OptionType(Pet2Type))), resolve = _.value.eitherPets), Field("favouritePet", PetType, resolve = _.value.pets.flatMap(_.headOption.flatMap(identity)).get), Field("favouritePetList", ListType(PetType), resolve = _.value.pets.getOrElse(Nil).flatMap(x ⇒ x).toSeq), Field("favouritePetOpt", OptionType(PetType), resolve = _.value.pets.flatMap(_.headOption.flatMap(identity))), @@ -131,6 +139,47 @@ class UnionInterfaceSpec extends WordSpec with Matchers with FutureResultSupport validateQuery = false ) + "executes using mapped union types" in check( + bob, + """ + { + __typename + name + favouritePet {name} + favouritePetOpt {name} + pets { + __typename + name + barks + meows + } + pets2 { + __typename + name + barks + meows + } + } + """, + Map( + "data" → Map( + "__typename" → "Person", + "name" → "Bob", + "favouritePet" → Map("name" → "Garfield"), + "favouritePetOpt" → Map("name" → "Garfield"), + "pets" → List( + Map("__typename" → "Cat", "name" → "Garfield", "meows" → false), + Map("__typename" → "Dog", "name" → "Odie", "barks" → true) + ), + "pets2" → List( + Map("__typename" → "Cat", "name" → "Garfield", "meows" → false), + Map("__typename" → "Dog", "name" → "Odie", "barks" → true) + ) + ) + ) , + validateQuery = false + ) + "executes union types with inline fragments" in check( bob, """ From f589aea2cdc1ed75bdfc9172d82c4db9af8d4fdf Mon Sep 17 00:00:00 2001 From: Dan Di Spaltro Date: Wed, 29 Aug 2018 19:29:22 -0700 Subject: [PATCH 2/2] Address map vs mapValue - Also fix some code reuse with AbstractTypes --- .../scala/sangria/execution/Resolver.scala | 26 ++++++++----------- src/main/scala/sangria/schema/Schema.scala | 2 +- .../execution/UnionInterfaceSpec.scala | 2 +- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/main/scala/sangria/execution/Resolver.scala b/src/main/scala/sangria/execution/Resolver.scala index 6baa15c2..10a6eeb2 100644 --- a/src/main/scala/sangria/execution/Resolver.scala +++ b/src/main/scala/sangria/execution/Resolver.scala @@ -901,26 +901,22 @@ class Resolver[Ctx]( resolveActionsPar(path, obj, actions, userCtx, fields.namesOrdered) case Failure(error) ⇒ Result(ErrorRegistry(path, error), None) } - case abst: MappedAbstractType[Any @unchecked] ⇒ - val newValue = abst.contraMap(value) - if (isUndefinedValue(value)) - Result(ErrorRegistry.empty, None) - else - abst.typeOf(newValue, schema) match { - case Some(obj) ⇒ resolveValue(path, astFields, obj, field, newValue, userCtx) - case None ⇒ Result(ErrorRegistry(path, - UndefinedConcreteTypeError(path, abst, schema.possibleTypes.getOrElse(abst.name, Vector.empty), newValue, exceptionHandler, sourceMapper, astFields.head.location.toList)), None) - } - case abst: AbstractType ⇒ if (isUndefinedValue(value)) Result(ErrorRegistry.empty, None) - else - abst.typeOf(value, schema) match { - case Some(obj) ⇒ resolveValue(path, astFields, obj, field, value, userCtx) + else { + val actualValue = + abst match { + case abst: MappedAbstractType[Any @unchecked] ⇒ abst.contraMap(value) + case _ ⇒ value + } + + abst.typeOf(actualValue, schema) match { + case Some(obj) ⇒ resolveValue(path, astFields, obj, field, actualValue, userCtx) case None ⇒ Result(ErrorRegistry(path, - UndefinedConcreteTypeError(path, abst, schema.possibleTypes.getOrElse(abst.name, Vector.empty), value, exceptionHandler, sourceMapper, astFields.head.location.toList)), None) + UndefinedConcreteTypeError(path, abst, schema.possibleTypes.getOrElse(abst.name, Vector.empty), actualValue, exceptionHandler, sourceMapper, astFields.head.location.toList)), None) } + } } def isUndefinedValue(value: Any) = diff --git a/src/main/scala/sangria/schema/Schema.scala b/src/main/scala/sangria/schema/Schema.scala index bf94bd65..b9742446 100644 --- a/src/main/scala/sangria/schema/Schema.scala +++ b/src/main/scala/sangria/schema/Schema.scala @@ -299,7 +299,7 @@ case class UnionType[Ctx]( astNodes: Vector[ast.AstNode] = Vector.empty) extends OutputType[Any] with CompositeType[Any] with AbstractType with NullableType with UnmodifiedType with HasAstInfo { def rename(newName: String) = copy(name = newName).asInstanceOf[this.type] def toAst: ast.TypeDefinition = SchemaRenderer.renderType(this) - def map[T](func: T => Any): OutputType[T] with MappedAbstractType[T] = new UnionType[Ctx](name, description, types, astDirectives, astNodes) with MappedAbstractType[T] { + def mapValue[T](func: T => Any): OutputType[T] with MappedAbstractType[T] = new UnionType[Ctx](name, description, types, astDirectives, astNodes) with MappedAbstractType[T] { override def contraMap(value: T): Any = func(value) }.asInstanceOf[OutputType[T] with MappedAbstractType[T]] } diff --git a/src/test/scala/sangria/execution/UnionInterfaceSpec.scala b/src/test/scala/sangria/execution/UnionInterfaceSpec.scala index 5fd0c8fd..40754cdb 100644 --- a/src/test/scala/sangria/execution/UnionInterfaceSpec.scala +++ b/src/test/scala/sangria/execution/UnionInterfaceSpec.scala @@ -31,7 +31,7 @@ class UnionInterfaceSpec extends WordSpec with Matchers with FutureResultSupport val PetType = UnionType[Unit]("Pet", types = DogType :: CatType :: Nil) - val Pet2Type = UnionType[Unit]("Pet2", types = DogType :: CatType :: Nil).map[Either[Dog, Cat]](_.fold(dog => dog: Any, cat => cat: Any)) + val Pet2Type = UnionType[Unit]("Pet2", types = DogType :: CatType :: Nil).mapValue[Either[Dog, Cat]](_.fold(dog => dog: Any, cat => cat: Any)) val PersonType = ObjectType("Person", interfaces[Unit, Person](NamedType), fields[Unit, Person]( Field("pets", OptionType(ListType(OptionType(PetType))), resolve = _.value.pets),