Skip to content

Commit

Permalink
Better support for custom directives
Browse files Browse the repository at this point in the history
Prototype for #913
In this prototype, we check how we can apply custom directives much easier.
Furthermore, we check if we can prove type safety, to that a custom directive
that can be applied on some elements cannot be applied on others.

Limitations:
- a directive can be applied on a field definition for example. With that current approach,
we cannot formulate that with the type system as a field definition lives in sangria.ast, and
we only handle sangria.schema types.
- we are introducing new types to mark on which elements a directive can be applied. Those types
are kind of duplication of the current [sangria.schema.DirectiveLocation values](https://github.com/sangria-graphql/sangria/blob/f339b5df97bd89c2a24fcfc977a1f20191ffd7fc/modules/core/src/main/scala/sangria/schema/Schema.scala#L1136-L1158).
  • Loading branch information
yanns committed Oct 6, 2022
1 parent f339b5d commit 67adcbb
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 7 deletions.
3 changes: 2 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ lazy val core = project
ProblemFilters.exclude[MissingTypesProblem]("sangria.schema.Directive$"),
ProblemFilters.exclude[MissingTypesProblem]("sangria.schema.MappedAbstractType"),
ProblemFilters.exclude[IncompatibleMethTypeProblem](
"sangria.execution.Resolver.resolveSimpleListValue")
"sangria.execution.Resolver.resolveSimpleListValue"),
ProblemFilters.exclude[DirectMissingMethodProblem]("sangria.schema.Field.apply")
),
Test / testOptions += Tests.Argument(TestFrameworks.ScalaTest, "-oF"),
libraryDependencies ++= Seq(
Expand Down
2 changes: 1 addition & 1 deletion modules/ast/src/main/scala/sangria/ast/QueryAst.scala
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ sealed trait WithDirectives extends AstNode {

case class Directive(
name: String,
arguments: Vector[Argument],
arguments: Vector[Argument] = Vector.empty,
comments: Vector[Comment] = Vector.empty,
location: Option[AstLocation] = None)
extends AstNode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package sangria.schema
import sangria.ast
import sangria.execution.FieldTag
import sangria.marshalling.{FromInput, MarshallerCapability, ScalarValueInfo, ToInput}
import sangria.schema.DirectiveLocationValue.On
import sangria.schema.InputObjectType.DefaultInput
import sangria.util.tag.@@
import sangria.validation.Violation

import scala.reflect.ClassTag
Expand Down Expand Up @@ -570,7 +572,8 @@ class DefaultAstSchemaBuilder[Ctx] extends AstSchemaBuilder[Ctx] {
deprecationReason = fieldDeprecationReason(definition),
complexity = fieldComplexity(typeDefinition, definition),
manualPossibleTypes = () => Nil,
astDirectives = definition.directives,
astDirectives = definition.directives
.asInstanceOf[Vector[ast.Directive with On[DirectiveLocationValue.Field.type]]],
astNodes = Vector(definition)
))

Expand Down
24 changes: 20 additions & 4 deletions modules/core/src/main/scala/sangria/schema/Schema.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import sangria.{ast, introspection}
import sangria.validation._
import sangria.introspection._
import sangria.renderer.{QueryRenderer, SchemaFilter, SchemaRenderer}
import sangria.schema.DirectiveLocationValue.On
import sangria.schema.InputObjectType.DefaultInput
import sangria.streaming.SubscriptionStreamLike

Expand Down Expand Up @@ -594,7 +595,7 @@ case class Field[Ctx, Val](
tags: List[FieldTag],
complexity: Option[(Ctx, Args, Double) => Double],
manualPossibleTypes: () => List[ObjectType[_, _]],
astDirectives: Vector[ast.Directive],
astDirectives: Vector[ast.Directive with On[DirectiveLocationValue.Field.type]],
astNodes: Vector[ast.AstNode])
extends Named
with HasArguments
Expand All @@ -618,8 +619,9 @@ object Field {
possibleTypes: => List[PossibleObject[_, _]] = Nil,
tags: List[FieldTag] = Nil,
complexity: Option[(Ctx, Args, Double) => Double] = None,
deprecationReason: Option[String] = None)(implicit
ev: ValidOutType[Res, Out]): Field[Ctx, Val] =
deprecationReason: Option[String] = None,
astDirectives: Vector[ast.Directive with On[DirectiveLocationValue.Field.type]] = Vector.empty
)(implicit ev: ValidOutType[Res, Out]): Field[Ctx, Val] =
Field[Ctx, Val](
name,
fieldType,
Expand All @@ -630,7 +632,7 @@ object Field {
tags,
complexity,
() => possibleTypes.map(_.objectType),
Vector.empty,
astDirectives,
Vector.empty)

def subs[Ctx, Val, StreamSource, Res, Out](
Expand Down Expand Up @@ -1133,6 +1135,20 @@ sealed trait HasArguments {
def arguments: List[Argument[_]]
}

sealed trait DirectiveLocationValue {
def spec: String
}
object DirectiveLocationValue {
trait On[V <: DirectiveLocationValue]

case object ArgumentDefinition extends DirectiveLocationValue {
override val spec: String = "ARGUMENT_DEFINITION"
}
case object Field extends DirectiveLocationValue {
override val spec: String = "FIELD"
}
}

object DirectiveLocation extends Enumeration {
val ArgumentDefinition: Value = Value
val Enum: Value = Value
Expand Down
145 changes: 145 additions & 0 deletions modules/core/src/test/scala/sangria/schema/CustomDirectiveSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package sangria.schema

import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import sangria.ast
import sangria.renderer.QueryRenderer
import sangria.schema.DirectiveLocationValue.On
import sangria.util.tag.@@

class CustomDirectiveSpec extends AnyWordSpec with Matchers {

case class Domain(value: Int)

private val AllDirective = new ast.Directive("field-directive")
with On[DirectiveLocationValue.Field.type with DirectiveLocationValue.ArgumentDefinition.type]

// with OnField
// with OnArgument
// with OnObjectType
// with OnInterfaceType

private val FieldDirective: ast.Directive with On[DirectiveLocationValue.Field.type] =
new ast.Directive("field-directive") with On[DirectiveLocationValue.Field.type]

// private val ArgumentDirective = new ast.Directive("arg-directive") with OnArgument
//
// private val ObjectDirective = new ast.Directive("object-directive") with OnObjectType
//
// private val InterfaceDirective = new ast.Directive("interface-directive") with OnInterfaceType
//
// private val CustomDirective = ast.Directive("custom-directive")

private val resolve: Context[Unit, Domain] => Action[Unit, Int] = _.value.value

"custom directive" when {
"in context of a Field" should {
"be applied if marked with OnField" in {
fields[Unit, Domain](
Field("field", IntType, resolve = resolve, astDirectives = Vector(FieldDirective)))

fields[Unit, Domain](
Field("field", IntType, resolve = resolve, astDirectives = Vector(AllDirective)))

fields[Unit, Domain](
Field(
"field",
IntType,
resolve = resolve,
astDirectives = Vector(FieldDirective, AllDirective)))

Field("field", IntType, resolve = resolve, astDirectives = Vector(FieldDirective)): Field[
Unit,
Domain]

// val field = (Field("field", IntType, resolve = resolve): Field[Unit, Domain])
// .withDirective(FieldDirective)
// .withDirectives(FieldDirective, AllDirective)
// field.astDirectives should be(Vector(FieldDirective, FieldDirective, AllDirective))
}

"not be applied if not marked with OnField" in {
assertTypeError("""
|fields[Unit, Domain](
| Field("field", IntType, resolve = resolve, astDirectives = Vector(CustomDirective)))
|""".stripMargin)

assertTypeError("""
|val field: Field[Unit, Domain] =
| Field("field", IntType, resolve = resolve, astDirectives = Vector(CustomDirective))
|""".stripMargin)
}

// "be combined with the @deprecated directive" in {
// val field = (Field(
// "field",
// IntType,
// resolve = resolve,
// deprecationReason = Some("use field2")): Field[Unit, Domain])
// .withDirective(FieldDirective)
//
// field.astDirectives should be(Vector(FieldDirective))
// QueryRenderer.renderPretty(field.toAst) should equal(
// """field: Int! @field-directive @deprecated(reason: "use field2")""")
// }
}
}

// "in context of an Argument" should {
// "be applied if marked with OnArgument" in {
// Argument("name", IntType, 42, astDirectives = Vector(ArgumentDirective))
// Argument("name", IntType, 42, astDirectives = Vector(AllDirective))
// Argument("name", IntType, 42).withDirective(ArgumentDirective)
// val arg = Argument("name", IntType, 42)
// .withDirective(AllDirective)
// .withDirectives(ArgumentDirective, ArgumentDirective)
// arg.astDirectives should be(Vector(AllDirective, ArgumentDirective, ArgumentDirective))
// }
//
// "not be applied if not marked with OnArgument" in {
// assertTypeError("""
// |Argument("name", IntType, 42, astDirectives = Vector(FieldDirective))
// |""".stripMargin)
// assertTypeError("""
// |Argument("name", IntType, 42).withDirective(FieldDirective)
// |""".stripMargin)
// }
// }
//
// "in context of an ObjectType" should {
// "be applied if marked with OnObjectType" in {
// val obj = ObjectType[Unit, Domain]("name", fields[Unit, Domain]())
// .withDirective(ObjectDirective)
// .withDirectives(AllDirective, ObjectDirective)
// obj.astDirectives should be(Vector(ObjectDirective, AllDirective, ObjectDirective))
// }
//
// "not be applied if not marked with OnObjectType" in {
// assertTypeError("""
// |ObjectType[Unit, Domain]("name", fields[Unit, Domain]()).withDirective(CustomDirective)
// |""".stripMargin)
// assertTypeError("""
// |ObjectType[Unit, Domain]("name", fields[Unit, Domain]()).withDirective(FieldDirective)
// |""".stripMargin)
// }
// }
//
// "in context of an InterfaceType" should {
// "be applied if marked with OnInterfaceType" in {
// val interface = InterfaceType[Unit, Domain]("name", fields[Unit, Domain]())
// .withDirective(InterfaceDirective)
// .withDirectives(AllDirective, InterfaceDirective)
// interface.astDirectives should be(
// Vector(InterfaceDirective, AllDirective, InterfaceDirective))
// }
//
// "not be applied if not marked with OnObjectType" in {
// assertTypeError("""
// |InterfaceType[Unit, Domain]("name", fields[Unit, Domain]()).withDirective(CustomDirective)
// |""".stripMargin)
// assertTypeError("""
// |InterfaceType[Unit, Domain]("name", fields[Unit, Domain]()).withDirective(FieldDirective)
// |""".stripMargin)
// }
// }
}

0 comments on commit 67adcbb

Please sign in to comment.