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

Add directive support #225

Merged
merged 7 commits into from
Feb 22, 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
116 changes: 83 additions & 33 deletions core/src/main/scala/caliban/Rendering.scala
Original file line number Diff line number Diff line change
@@ -1,44 +1,66 @@
package caliban

import caliban.Value.{ BooleanValue, EnumValue, FloatValue, IntValue, NullValue, StringValue }
import caliban.introspection.adt._
import caliban.parsing.adt.Directive

object Rendering {

private implicit val kindOrdering: Ordering[__TypeKind] = Ordering
.by[__TypeKind, Int] {
case __TypeKind.SCALAR => 1
case __TypeKind.NON_NULL => 2
case __TypeKind.LIST => 3
case __TypeKind.UNION => 4
case __TypeKind.ENUM => 5
case __TypeKind.INPUT_OBJECT => 6
case __TypeKind.INTERFACE => 7
case __TypeKind.OBJECT => 8
}

private implicit val renderOrdering: Ordering[(String, __Type)] = Ordering.by(o => (o._2.kind, o._2.name))

/**
* Returns a string that renders the provided types into the GraphQL format.
*/
def renderTypes(types: Map[String, __Type]): String =
types.flatMap {
case (_, t) =>
t.kind match {
case __TypeKind.SCALAR => t.name.flatMap(name => if (isBuiltinScalar(name)) None else Some(s"scalar $name"))
case __TypeKind.NON_NULL => None
case __TypeKind.LIST => None
case __TypeKind.UNION =>
val renderedTypes: String =
t.possibleTypes
.fold(List.empty[String])(_.flatMap(_.name))
.mkString(" | ")
Some(s"""${renderDescription(t.description)}${renderKind(t.kind)} ${renderTypeName(t)} = $renderedTypes""")
case _ =>
val renderedFields: String = t
.fields(__DeprecatedArgs())
.fold(List.empty[String])(_.map(renderField))
.mkString("\n ")
val renderedInputFields: String = t.inputFields
.fold(List.empty[String])(_.map(renderInputValue))
.mkString("\n ")
val renderedEnumValues = t
.enumValues(__DeprecatedArgs())
.fold(List.empty[String])(_.map(renderEnumValue))
.mkString("\n ")
Some(
s"""${renderDescription(t.description)}${renderKind(t.kind)} ${renderTypeName(t)}${renderInterfaces(t)} {
| $renderedFields$renderedInputFields$renderedEnumValues
|}""".stripMargin
)
}
}.mkString("\n\n")
types.toList
.sorted(renderOrdering)
.flatMap {
case (_, t) =>
t.kind match {
case __TypeKind.SCALAR => t.name.flatMap(name => if (isBuiltinScalar(name)) None else Some(s"scalar $name"))
case __TypeKind.NON_NULL => None
case __TypeKind.LIST => None
case __TypeKind.UNION =>
val renderedTypes: String =
t.possibleTypes
.fold(List.empty[String])(_.flatMap(_.name))
.mkString(" | ")
Some(
s"""${renderDescription(t.description)}${renderKind(t.kind)} ${renderTypeName(t)} = $renderedTypes"""
)
case _ =>
val renderedDirectives: String = renderDirectives(t.directives)
val renderedFields: String = t
.fields(__DeprecatedArgs())
.fold(List.empty[String])(_.map(renderField))
.mkString("\n ")
val renderedInputFields: String = t.inputFields
.fold(List.empty[String])(_.map(renderInputValue))
.mkString("\n ")
val renderedEnumValues = t
.enumValues(__DeprecatedArgs())
.fold(List.empty[String])(_.map(renderEnumValue))
.mkString("\n ")
Some(
s"""${renderDescription(t.description)}${renderKind(t.kind)} ${renderTypeName(t)}${renderInterfaces(t)}$renderedDirectives {
| $renderedFields$renderedInputFields$renderedEnumValues
|}""".stripMargin
)
}
}
.mkString("\n\n")

private def renderInterfaces(t: __Type): String =
t.interfaces()
Expand All @@ -62,13 +84,41 @@ object Rendering {
case Some(value) => if (value.contains("\n")) s"""\"\"\"\n$value\"\"\"\n""" else s""""$value"\n"""
}

private def renderDirectiveArgument(value: InputValue): Option[String] = value match {
case InputValue.ListValue(values) =>
Some(values.flatMap(renderDirectiveArgument).mkString("[", ",", "]"))
case InputValue.ObjectValue(fields) =>
Some(
fields.map { case (key, value) => renderDirectiveArgument(value).map(v => s"$key: $v") }.mkString("{", ",", "}")
)
case NullValue => Some("null")
case StringValue(value) => Some("\"" + value + "\"")
case i: IntValue => Some(i.toInt.toString)
case f: FloatValue => Some(f.toFloat.toString)
case BooleanValue(value) => Some(value.toString)
case EnumValue(value) => Some(value)
case InputValue.VariableValue(_) => None
}

private def renderDirective(directive: Directive) =
s"@${directive.name}${if (directive.arguments.nonEmpty) s"""(${directive.arguments.flatMap {
case (key, value) => renderDirectiveArgument(value).map(v => s"$key: $v")
}.mkString("{", ",", "}")})"""
else ""}"

private def renderDirectives(directives: Option[List[Directive]]) =
directives.fold("") {
case Nil => ""
case d => d.map(renderDirective).mkString(" ", " ", "")
}

private def renderField(field: __Field): String =
s"${field.name}${renderArguments(field.args)}: ${renderTypeName(field.`type`())}${if (field.isDeprecated)
s" @deprecated${field.deprecationReason.fold("")(reason => s"""(reason: "$reason")""")}"
else ""}"
else ""}${renderDirectives(field.directives)}"

private def renderInputValue(inputValue: __InputValue): String =
s"${inputValue.name}: ${renderTypeName(inputValue.`type`())}${inputValue.defaultValue.fold("")(d => s" = $d")}"
s"${inputValue.name}: ${renderTypeName(inputValue.`type`())}${inputValue.defaultValue.fold("")(d => s" = $d")}${renderDirectives(inputValue.directives)}"

private def renderEnumValue(v: __EnumValue): String =
s"${renderDescription(v.description)}${v.name}${if (v.isDeprecated)
Expand Down
12 changes: 6 additions & 6 deletions core/src/main/scala/caliban/execution/Executor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,16 @@ object Executor {
case ObjectStep(objectName, fields) =>
val mergedFields = mergeFields(currentField, objectName)
val items = mergedFields.map {
case f @ Field(name @ "__typename", _, _, alias, _, _, _, _) =>
(alias.getOrElse(name), PureStep(StringValue(objectName)), fieldInfo(f, path))
case f @ Field(name, _, _, alias, _, _, args, _) =>
case f @ Field(name @ "__typename", _, _, alias, _, _, _, _, directives) =>
(alias.getOrElse(name), PureStep(StringValue(objectName)), fieldInfo(f, path, directives))
case f @ Field(name, _, _, alias, _, _, args, _, directives) =>
val arguments = resolveVariables(args, request.variableDefinitions, variables)
(
alias.getOrElse(name),
fields
.get(name)
.fold(NullStep: ReducedStep[R])(reduceStep(_, f, arguments, Left(alias.getOrElse(name)) :: path)),
fieldInfo(f, path)
fieldInfo(f, path, directives)
)
}
reduceObject(items, fieldWrappers)
Expand Down Expand Up @@ -182,8 +182,8 @@ object Executor {
.toList
}

private def fieldInfo(field: Field, path: List[Either[String, Int]]): FieldInfo =
FieldInfo(field.alias.getOrElse(field.name), path, field.parentType, field.fieldType)
private def fieldInfo(field: Field, path: List[Either[String, Int]], fieldDirectives: List[Directive]): FieldInfo =
FieldInfo(field.alias.getOrElse(field.name), path, field.parentType, field.fieldType, fieldDirectives)

private def reduceList[R](list: List[ReducedStep[R]]): ReducedStep[R] =
if (list.forall(_.isInstanceOf[PureStep]))
Expand Down
19 changes: 14 additions & 5 deletions core/src/main/scala/caliban/execution/Field.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ case class Field(
fields: List[Field] = Nil,
conditionalFields: Map[String, List[Field]] = Map(),
arguments: Map[String, InputValue] = Map(),
locationInfo: LocationInfo = LocationInfo.origin
locationInfo: LocationInfo = LocationInfo.origin,
directives: List[Directive] = List.empty
)

object Field {
Expand All @@ -26,17 +27,24 @@ object Field {
fragments: Map[String, FragmentDefinition],
variableValues: Map[String, InputValue],
fieldType: __Type,
sourceMapper: SourceMapper
sourceMapper: SourceMapper,
directives: List[Directive]
): Field = {

def loop(selectionSet: List[Selection], fieldType: __Type): Field = {
val innerType = Types.innerType(fieldType)
val (fields, cFields) = selectionSet.map {
case f @ F(alias, name, arguments, _, selectionSet, index) if checkDirectives(f.directives, variableValues) =>
val t = innerType
case F(alias, name, arguments, directives, selectionSet, index)
if checkDirectives(directives, variableValues) =>
val selected = innerType
.fields(__DeprecatedArgs(Some(true)))
.flatMap(_.find(_.name == name))

val schemaDirectives = selected.flatMap(_.directives).getOrElse(Nil)

val t = selected
.fold(Types.string)(_.`type`()) // default only case where it's not found is __typename

val field = loop(selectionSet, t)
(
List(
Expand All @@ -48,7 +56,8 @@ object Field {
field.fields,
field.conditionalFields,
arguments,
sourceMapper.getLocation(index)
sourceMapper.getLocation(index),
directives ++ schemaDirectives
)
),
Map.empty[String, List[Field]]
Expand Down
9 changes: 8 additions & 1 deletion core/src/main/scala/caliban/execution/FieldInfo.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
package caliban.execution

import caliban.introspection.adt.__Type
import caliban.parsing.adt.Directive

case class FieldInfo(fieldName: String, path: List[Either[String, Int]], parentType: Option[__Type], returnType: __Type)
case class FieldInfo(
fieldName: String,
path: List[Either[String, Int]],
parentType: Option[__Type],
returnType: __Type,
directives: List[Directive] = Nil
)
8 changes: 7 additions & 1 deletion core/src/main/scala/caliban/introspection/Introspector.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,13 @@ object Introspector {
def introspect(rootType: RootType): RootSchema[Any] = {
val types = rootType.types.updated("Boolean", Types.boolean).values.toList.sortBy(_.name.getOrElse(""))
val resolver = __Introspection(
__Schema(rootType.queryType, rootType.mutationType, rootType.subscriptionType, types, directives),
__Schema(
rootType.queryType,
rootType.mutationType,
rootType.subscriptionType,
types,
directives ++ rootType.additionalDirectives
),
args => types.find(_.name.contains(args.name)).get
)
val introspectionSchema = Schema.gen[__Introspection]
Expand Down
5 changes: 4 additions & 1 deletion core/src/main/scala/caliban/introspection/adt/__Field.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package caliban.introspection.adt

import caliban.parsing.adt.Directive

case class __Field(
name: String,
description: Option[String],
args: List[__InputValue],
`type`: () => __Type,
isDeprecated: Boolean = false,
deprecationReason: Option[String] = None
deprecationReason: Option[String] = None,
directives: Option[List[Directive]] = None
)
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
package caliban.introspection.adt

case class __InputValue(name: String, description: Option[String], `type`: () => __Type, defaultValue: Option[String])
import caliban.parsing.adt.Directive

case class __InputValue(
name: String,
description: Option[String],
`type`: () => __Type,
defaultValue: Option[String],
directives: Option[List[Directive]] = None
)
8 changes: 6 additions & 2 deletions core/src/main/scala/caliban/introspection/adt/__Type.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package caliban.introspection.adt

import caliban.parsing.adt.Directive

case class __Type(
kind: __TypeKind,
name: Option[String] = None,
Expand All @@ -9,7 +11,8 @@ case class __Type(
possibleTypes: Option[List[__Type]] = None,
enumValues: __DeprecatedArgs => Option[List[__EnumValue]] = _ => None,
inputFields: Option[List[__InputValue]] = None,
ofType: Option[__Type] = None
ofType: Option[__Type] = None,
directives: Option[List[Directive]] = None
) {
def |+|(that: __Type): __Type = __Type(
kind,
Expand All @@ -23,6 +26,7 @@ case class __Type(
(enumValues(args) ++ that.enumValues(args))
.reduceOption((a, b) => a.filterNot(v => b.exists(_.name == v.name)) ++ b),
(inputFields ++ that.inputFields).reduceOption((a, b) => a.filterNot(t => b.exists(_.name == t.name)) ++ b),
(ofType ++ that.ofType).reduceOption(_ |+| _)
(ofType ++ that.ofType).reduceOption(_ |+| _),
(directives ++ that.directives).reduceOption(_ ++ _)
)
}
2 changes: 1 addition & 1 deletion core/src/main/scala/caliban/parsing/adt/Directive.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ package caliban.parsing.adt

import caliban.InputValue

case class Directive(name: String, arguments: Map[String, InputValue], index: Int)
case class Directive(name: String, arguments: Map[String, InputValue] = Map.empty, index: Int = 0)
7 changes: 7 additions & 0 deletions core/src/main/scala/caliban/schema/Annotations.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package caliban.schema

import caliban.parsing.adt.Directive

import scala.annotation.StaticAnnotation

object Annotations {
Expand All @@ -25,6 +27,11 @@ object Annotations {
*/
case class GQLName(value: String) extends StaticAnnotation

/**
* Annotation used to provide directives to a schema type
*/
case class GQLDirective(directive: Directive) extends StaticAnnotation

/**
* Annotation to make a sealed trait an interface instead of a union type
*/
Expand Down
9 changes: 7 additions & 2 deletions core/src/main/scala/caliban/schema/RootType.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package caliban.schema

import caliban.introspection.adt.__Type
import caliban.introspection.adt.{ __Directive, __Type }
import caliban.schema.Types.collectTypes

case class RootType(queryType: __Type, mutationType: Option[__Type], subscriptionType: Option[__Type]) {
case class RootType(
queryType: __Type,
mutationType: Option[__Type],
subscriptionType: Option[__Type],
additionalDirectives: List[__Directive] = List.empty
) {
val empty = Map.empty[String, __Type]
val types: Map[String, __Type] =
collectTypes(queryType) ++
Expand Down
Loading