Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interface support #223

Merged
merged 1 commit into from
Feb 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions core/src/main/scala/caliban/Rendering.scala
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,28 @@ object Rendering {
.enumValues(__DeprecatedArgs())
.fold(List.empty[String])(_.map(renderEnumValue))
.mkString("\n ")
Some(s"""${renderDescription(t.description)}${renderKind(t.kind)} ${renderTypeName(t)} {
| $renderedFields$renderedInputFields$renderedEnumValues
|}""".stripMargin)
Some(
s"""${renderDescription(t.description)}${renderKind(t.kind)} ${renderTypeName(t)}${renderInterfaces(t)} {
| $renderedFields$renderedInputFields$renderedEnumValues
|}""".stripMargin
)
}
}.mkString("\n\n")

private def renderInterfaces(t: __Type): String =
t.interfaces()
.fold("")(_.flatMap(_.name) match {
case Nil => ""
case list => s" implements ${list.mkString("& ")}"
})

private def renderKind(kind: __TypeKind): String =
kind match {
case __TypeKind.OBJECT => "type"
case __TypeKind.UNION => "union"
case __TypeKind.ENUM => "enum"
case __TypeKind.INPUT_OBJECT => "input"
case __TypeKind.INTERFACE => "interface"
case _ => ""
}

Expand Down
4 changes: 2 additions & 2 deletions core/src/main/scala/caliban/introspection/adt/__Type.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ case class __Type(
name: Option[String] = None,
description: Option[String] = None,
fields: __DeprecatedArgs => Option[List[__Field]] = _ => None,
interfaces: Option[List[__Type]] = None,
interfaces: () => Option[List[__Type]] = () => None,
possibleTypes: Option[List[__Type]] = None,
enumValues: __DeprecatedArgs => Option[List[__EnumValue]] = _ => None,
inputFields: Option[List[__InputValue]] = None,
Expand All @@ -17,7 +17,7 @@ case class __Type(
(description ++ that.description).reduceOption((_, b) => b),
args =>
(fields(args) ++ that.fields(args)).reduceOption((a, b) => a.filterNot(f => b.exists(_.name == f.name)) ++ b),
(interfaces ++ that.interfaces).reduceOption((a, b) => a.filterNot(t => b.exists(_.name == t.name)) ++ b),
() => (interfaces() ++ that.interfaces()).reduceOption((a, b) => a.filterNot(t => b.exists(_.name == t.name)) ++ b),
(possibleTypes ++ that.possibleTypes).reduceOption((a, b) => a.filterNot(t => b.exists(_.name == t.name)) ++ b),
args =>
(enumValues(args) ++ that.enumValues(args))
Expand Down
5 changes: 5 additions & 0 deletions core/src/main/scala/caliban/schema/Annotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,9 @@ object Annotations {
* Annotation used to provide an alternative name to a field or a type.
*/
case class GQLName(value: String) extends StaticAnnotation

/**
* Annotation to make a sealed trait an interface instead of a union type
*/
case class GQLInterface() extends StaticAnnotation
}
35 changes: 26 additions & 9 deletions core/src/main/scala/caliban/schema/Schema.scala
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package caliban.schema

import java.util.UUID

import scala.annotation.implicitNotFound
import scala.concurrent.Future
import scala.language.experimental.macros

import caliban.ResponseValue._
import caliban.Value._
import caliban.introspection.adt._
import caliban.schema.Annotations.{ GQLDeprecated, GQLDescription, GQLInputName, GQLName }
import caliban.schema.Annotations.{ GQLDeprecated, GQLDescription, GQLInputName, GQLInterface, GQLName }
import caliban.schema.Step._
import caliban.schema.Types._
import caliban.{ InputValue, ResponseValue }
Expand Down Expand Up @@ -357,12 +355,31 @@ trait DerivationSchema[R] {
)
}
)
else
makeUnion(
Some(getName(ctx)),
getDescription(ctx),
subtypes.map { case (t, _) => fixEmptyUnionObject(t) }
)
else {
ctx.annotations.collectFirst {
case GQLInterface() => ()
}.fold(
makeUnion(
Some(getName(ctx)),
getDescription(ctx),
subtypes.map { case (t, _) => fixEmptyUnionObject(t) }
)
) { _ =>
val impl = subtypes.map(_._1.copy(interfaces = () => Some(List(toType(isInput)))))
val commonFields = impl
.flatMap(_.fields(__DeprecatedArgs(Some(true))))
.flatten
.groupBy(_.name)
.collect {
case (name, list)
if impl.forall(_.fields(__DeprecatedArgs(Some(true))).getOrElse(Nil).exists(_.name == name)) =>
list.headOption
}
.flatten

makeInterface(Some(getName(ctx)), getDescription(ctx), commonFields.toList, impl)
}
}
}

// see https://github.com/graphql/graphql-spec/issues/568
Expand Down
31 changes: 23 additions & 8 deletions core/src/main/scala/caliban/schema/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ object Types {
/**
* Creates a new scalar type with the given name.
*/
def makeScalar(name: String, description: Option[String] = None) = __Type(__TypeKind.SCALAR, Some(name), description)
def makeScalar(name: String, description: Option[String] = None): __Type =
__Type(__TypeKind.SCALAR, Some(name), description)

val boolean: __Type = makeScalar("Boolean")
val string: __Type = makeScalar("String")
Expand All @@ -16,33 +17,47 @@ object Types {
val float: __Type = makeScalar("Float")
val double: __Type = makeScalar("Double")

def makeList(underlying: __Type) = __Type(__TypeKind.LIST, ofType = Some(underlying))
def makeList(underlying: __Type): __Type = __Type(__TypeKind.LIST, ofType = Some(underlying))

def makeNonNull(underlying: __Type) = __Type(__TypeKind.NON_NULL, ofType = Some(underlying))
def makeNonNull(underlying: __Type): __Type = __Type(__TypeKind.NON_NULL, ofType = Some(underlying))

def makeEnum(name: Option[String], description: Option[String], values: List[__EnumValue]) =
def makeEnum(name: Option[String], description: Option[String], values: List[__EnumValue]): __Type =
__Type(
__TypeKind.ENUM,
name,
description,
enumValues = args => Some(values.filter(v => args.includeDeprecated.getOrElse(false) || !v.isDeprecated))
)

def makeObject(name: Option[String], description: Option[String], fields: List[__Field]) =
def makeObject(name: Option[String], description: Option[String], fields: List[__Field]): __Type =
__Type(
__TypeKind.OBJECT,
name,
description,
fields = args => Some(fields.filter(v => args.includeDeprecated.getOrElse(false) || !v.isDeprecated)),
interfaces = Some(Nil)
interfaces = () => Some(Nil)
)

def makeInputObject(name: Option[String], description: Option[String], fields: List[__InputValue]) =
def makeInputObject(name: Option[String], description: Option[String], fields: List[__InputValue]): __Type =
__Type(__TypeKind.INPUT_OBJECT, name, description, inputFields = Some(fields))

def makeUnion(name: Option[String], description: Option[String], subTypes: List[__Type]) =
def makeUnion(name: Option[String], description: Option[String], subTypes: List[__Type]): __Type =
__Type(__TypeKind.UNION, name, description, possibleTypes = Some(subTypes))

def makeInterface(
name: Option[String],
description: Option[String],
fields: List[__Field],
subTypes: List[__Type]
): __Type =
__Type(
__TypeKind.INTERFACE,
name,
description,
fields = args => Some(fields.filter(v => args.includeDeprecated.getOrElse(false) || !v.isDeprecated)),
possibleTypes = Some(subTypes)
)

/**
* Returns a map of all the types nested within the given root type.
*/
Expand Down
10 changes: 9 additions & 1 deletion core/src/test/scala/caliban/TestUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@ package caliban

import caliban.TestUtils.Origin._
import caliban.TestUtils.Role._
import caliban.schema.Annotations.{ GQLDeprecated, GQLDescription }
import caliban.schema.Annotations.{ GQLDeprecated, GQLDescription, GQLInterface }
import caliban.schema.Schema
import zio.UIO
import zio.stream.ZStream

object TestUtils {

@GQLInterface
sealed trait Interface
object Interface {
case class A(id: String, other: Int) extends Interface
case class B(id: String) extends Interface
case class C(id: String, blah: Boolean) extends Interface
}

sealed trait Origin

object Origin {
Expand Down
13 changes: 12 additions & 1 deletion core/src/test/scala/caliban/execution/ExecutionSpec.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package caliban.execution

import java.util.UUID

import caliban.CalibanError.ExecutionError
import caliban.GraphQL._
import caliban.Macros.gqldoc
Expand Down Expand Up @@ -339,6 +338,18 @@ object ExecutionSpec
interpreter.execute(query).map(_.data.toString),
equalTo("""{"test":{"a":333}}""")
)
},
testM("test Interface") {
case class Test(i: Interface)
val interpreter = graphQL(RootResolver(Test(Interface.B("ok")))).interpreter
val query = gqldoc("""
{
i {
id
}
}""")

assertM(interpreter.execute(query).map(_.data.toString), equalTo("""{"i":{"id":"ok"}}"""))
}
)
)
6 changes: 5 additions & 1 deletion vuepress/docs/docs/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Make sure those implicits are in scope when you call `graphQL(...)`. This will m
This will also improve compilation times and generate less bytecode.
:::

## Enum and union
## Enums, unions, interfaces

A sealed trait will be converted to a different GraphQL type depending on its content:

Expand Down Expand Up @@ -104,6 +104,9 @@ type Mechanic {
}
```

If you prefer an `Interface` instead of a `Union` type, add the `@GQLInterface` annotation to your sealed trait.
An interface will be created with all the fields that are common to the case classes extending the sealed trait.

## Arguments

To declare a field that take arguments, create a dedicated case class representing the arguments and make the field a _function_ from this class to the result type.
Expand Down Expand Up @@ -153,6 +156,7 @@ Caliban supports a few annotations to enrich data types:
- `@GQLInputName("name")` allows you to specify a different name for a data type used as an input (by default, the suffix `Input` is appended to the type name).
- `@GQLDescription("description")` lets you provide a description for a data type or field. This description will be visible when your schema is introspected.
- `@GQLDeprecated("reason")` allows deprecating a field or an enum value.
- `@GQLInterface` to force a sealed trait generating an interface instead of a union

## Custom types

Expand Down