Skip to content

Commit

Permalink
Merge pull request #1064 from asteinwedel/input-deprecation-tracker
Browse files Browse the repository at this point in the history
Input deprecation tracker
  • Loading branch information
yanns authored Oct 16, 2023
2 parents b8b12fc + 95b19c4 commit 6d14691
Show file tree
Hide file tree
Showing 14 changed files with 543 additions and 37 deletions.
48 changes: 47 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,53 @@ lazy val core = project
ProblemFilters.exclude[DirectMissingMethodProblem]("sangria.schema.InputField.this"),
ProblemFilters.exclude[DirectMissingMethodProblem]("sangria.schema.InputField.apply"),
ProblemFilters.exclude[ReversedMissingMethodProblem](
"sangria.schema.InputValue.deprecationReason")
"sangria.schema.InputValue.deprecationReason"),
ProblemFilters.exclude[ReversedMissingMethodProblem](
"sangria.execution.DeprecationTracker.deprecatedDirectiveArgUsed"),
ProblemFilters.exclude[ReversedMissingMethodProblem](
"sangria.execution.DeprecationTracker.deprecatedInputObjectFieldUsed"),
ProblemFilters.exclude[ReversedMissingMethodProblem](
"sangria.execution.DeprecationTracker.deprecatedFieldArgUsed"),
ProblemFilters.exclude[DirectMissingMethodProblem](
"sangria.execution.DeprecationTracker.empty"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"sangria.execution.Executor.<init>$default$5"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"sangria.execution.Executor.execute$default$10"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"sangria.execution.Executor.prepare$default$10"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"sangria.execution.Executor.deprecationTracker"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"sangria.execution.Executor.copy$default$5"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"sangria.execution.Executor.apply$default$5"),
ProblemFilters.exclude[IncompatibleResultTypeProblem]("sangria.execution.Executor._5"),
ProblemFilters.exclude[MissingClassProblem]("sangria.execution.NilDeprecationTracker"),
ProblemFilters.exclude[MissingClassProblem]("sangria.execution.NilDeprecationTracker$"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"sangria.execution.QueryReducerExecutor.reduceQueryWithoutVariables$default$8"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"sangria.execution.ValueCoercionHelper.<init>$default$2"),
ProblemFilters.exclude[IncompatibleMethTypeProblem](
"sangria.execution.ValueCoercionHelper.this"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"sangria.execution.ValueCoercionHelper.<init>$default$2"),
ProblemFilters.exclude[IncompatibleMethTypeProblem]("sangria.execution.ValueCollector.this"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"sangria.execution.batch.BatchExecutor.executeBatch$default$10"),
ProblemFilters.exclude[IncompatibleMethTypeProblem]("sangria.schema.Context.apply"),
ProblemFilters.exclude[IncompatibleMethTypeProblem]("sangria.schema.Context.this"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"sangria.schema.Context.deprecationTracker"),
ProblemFilters.exclude[IncompatibleMethTypeProblem]("sangria.schema.Context.copy"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"sangria.schema.Context.copy$default$10"),
ProblemFilters.exclude[IncompatibleResultTypeProblem]("sangria.schema.Context._10"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"sangria.schema.WithInputTypeRendering.deprecationTracker"),
ProblemFilters.exclude[ReversedMissingMethodProblem](
"sangria.schema.WithInputTypeRendering.deprecationTracker")
),
Test / testOptions += Tests.Argument(TestFrameworks.ScalaTest, "-oF"),
libraryDependencies ++= Seq(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ private[execution] class AsyncResolverBuilder[F[_]: Async](asyncToFuture: AsyncT
exceptionHandler: ExceptionHandler,
deferredResolver: DeferredResolver[Ctx],
sourceMapper: Option[SourceMapper],
deprecationTracker: DeprecationTracker,
deprecationTracker: Option[DeprecationTracker],
middleware: List[(Any, Middleware[Ctx])],
maxQueryDepth: Option[Int],
deferredResolverState: Any,
Expand Down Expand Up @@ -77,7 +77,7 @@ private[execution] class AsyncResolver[Ctx, F[_]: Async](
exceptionHandler: ExceptionHandler,
deferredResolver: DeferredResolver[Ctx],
sourceMapper: Option[SourceMapper],
deprecationTracker: DeprecationTracker,
deprecationTracker: Option[DeprecationTracker],
middleware: List[(Any, Middleware[Ctx])],
maxQueryDepth: Option[Int],
deferredResolverState: Any,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,51 @@
package sangria.execution

import sangria.schema.{Context, EnumType}
import sangria.schema._

trait DeprecationTracker {
def deprecatedFieldUsed[Ctx](ctx: Context[Ctx, _]): Unit
def deprecatedEnumValueUsed[T, Ctx](`enum`: EnumType[T], value: T, userContext: Ctx): Unit
def deprecatedDirectiveArgUsed[Ctx](
directive: Directive,
argument: Argument[_],
ctx: Context[Ctx, _]): Unit
def deprecatedInputObjectFieldUsed[T, Ctx](
inputObject: InputObjectType[T],
field: InputField[_],
ctx: Context[Ctx, _]): Unit
def deprecatedFieldArgUsed[Ctx](
argument: Argument[_],
ctx: Context[Ctx, _]
): Unit
}

object DeprecationTracker {
val empty = NilDeprecationTracker
val print = new LoggingDeprecationTracker(println)
}

object NilDeprecationTracker extends DeprecationTracker {
def deprecatedFieldUsed[Ctx](ctx: Context[Ctx, _]) = ()
def deprecatedEnumValueUsed[T, Ctx](`enum`: EnumType[T], value: T, userContext: Ctx) = ()
}

class LoggingDeprecationTracker(logFn: String => Unit) extends DeprecationTracker {
def deprecatedFieldUsed[Ctx](ctx: Context[Ctx, _]) =
logFn(
s"Deprecated field '${ctx.parentType.name}.${ctx.field.name}' used at path '${ctx.path}'.")

def deprecatedEnumValueUsed[T, Ctx](`enum`: EnumType[T], value: T, userContext: Ctx) =
logFn(s"Deprecated enum value '$value' used of enum '${`enum`.name}'.")

def deprecatedDirectiveArgUsed[Ctx](
directive: Directive,
argument: Argument[_],
ctx: Context[Ctx, _]) =
logFn(s"Deprecated argument '${argument.name}' used of directive '${directive.name}'.")

def deprecatedInputObjectFieldUsed[T, Ctx](
inputObject: InputObjectType[T],
field: InputField[_],
ctx: Context[Ctx, _]) = logFn(
s"Deprecated field '${field.name}' used of input object '${inputObject.name}'.")

def deprecatedFieldArgUsed[Ctx](
argument: Argument[_],
ctx: Context[Ctx, _]
) = logFn(
s"Deprecated argument '${argument.name}' used of field '${ctx.parentType.name}'.${ctx.field.name}'.")
}
6 changes: 3 additions & 3 deletions modules/core/src/main/scala/sangria/execution/Executor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ case class Executor[Ctx, Root](
queryValidator: QueryValidator = QueryValidator.default,
deferredResolver: DeferredResolver[Ctx] = DeferredResolver.empty,
exceptionHandler: ExceptionHandler = ExceptionHandler.empty,
deprecationTracker: DeprecationTracker = DeprecationTracker.empty,
deprecationTracker: Option[DeprecationTracker] = None,
middleware: List[Middleware[Ctx]] = Nil,
maxQueryDepth: Option[Int] = None,
queryReducers: List[QueryReducer[Ctx, _]] = Nil,
Expand Down Expand Up @@ -326,7 +326,7 @@ object Executor {
queryValidator: QueryValidator = QueryValidator.default,
deferredResolver: DeferredResolver[Ctx] = DeferredResolver.empty,
exceptionHandler: ExceptionHandler = ExceptionHandler.empty,
deprecationTracker: DeprecationTracker = DeprecationTracker.empty,
deprecationTracker: Option[DeprecationTracker] = None,
middleware: List[Middleware[Ctx]] = Nil,
maxQueryDepth: Option[Int] = None,
queryReducers: List[QueryReducer[Ctx, _]] = Nil,
Expand Down Expand Up @@ -358,7 +358,7 @@ object Executor {
queryValidator: QueryValidator = QueryValidator.default,
deferredResolver: DeferredResolver[Ctx] = DeferredResolver.empty,
exceptionHandler: ExceptionHandler = ExceptionHandler.empty,
deprecationTracker: DeprecationTracker = DeprecationTracker.empty,
deprecationTracker: Option[DeprecationTracker] = None,
middleware: List[Middleware[Ctx]] = Nil,
maxQueryDepth: Option[Int] = None,
queryReducers: List[QueryReducer[Ctx, _]] = Nil,
Expand Down
130 changes: 125 additions & 5 deletions modules/core/src/main/scala/sangria/execution/FutureResolver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import sangria.schema._
import sangria.streaming.SubscriptionStream

import scala.annotation.tailrec
import scala.collection.immutable.VectorBuilder
import scala.collection.immutable.{ListMap, VectorBuilder}
import scala.collection.mutable
import scala.concurrent.{ExecutionContext, Future, Promise}
import scala.util.control.NonFatal
import scala.util.{Failure, Success}
Expand All @@ -25,7 +26,7 @@ private[execution] object FutureResolverBuilder extends ResolverBuilder {
exceptionHandler: ExceptionHandler,
deferredResolver: DeferredResolver[Ctx],
sourceMapper: Option[SourceMapper],
deprecationTracker: DeprecationTracker,
deprecationTracker: Option[DeprecationTracker],
middleware: List[(Any, Middleware[Ctx])],
maxQueryDepth: Option[Int],
deferredResolverState: Any,
Expand Down Expand Up @@ -69,7 +70,7 @@ private[execution] class FutureResolver[Ctx](
exceptionHandler: ExceptionHandler,
deferredResolver: DeferredResolver[Ctx],
sourceMapper: Option[SourceMapper],
deprecationTracker: DeprecationTracker,
deprecationTracker: Option[DeprecationTracker],
middleware: List[(Any, Middleware[Ctx])],
maxQueryDepth: Option[Int],
deferredResolverState: Any,
Expand Down Expand Up @@ -1534,6 +1535,126 @@ private[execution] class FutureResolver[Ctx](
Result(errorReg, if (canceled) None else Some(marshaller.arrayNode(listBuilder.result())))
}

private def trackDeprecation(
deprecationTracker: DeprecationTracker,
ctx: Context[Ctx, _]): Unit = {
val fieldArgs = ctx.args
val visitedDirectives = mutable.Set[String]()

def getArgValue(name: String, args: Args): Option[_] =
if (args.argDefinedInQuery(name)) {
if (args.optionalArgs.contains(name)) {
args.argOpt(name)
} else {
Some(args.arg(name))
}
} else {
None
}

def deprecatedArgsUsed(argDefs: List[Argument[_]], argValues: Args): List[Argument[_]] =
argDefs.filter { argDef =>
val argValue = getArgValue(argDef.name, argValues)
argDef.deprecationReason.isDefined && argValue.isDefined
}

def trackDeprecatedDirectiveArgs(astDirective: ast.Directive): Unit = {
// prevent infinite loop from directiveA -> arg -> directiveA -> arg ...
if (visitedDirectives.contains(astDirective.name)) {
return
}
visitedDirectives.add(astDirective.name)

ctx.schema.directives.find(_.name == astDirective.name) match {
case Some(directive) =>
val directiveArgs = valueCollector
.getArgumentValues(
Some(astDirective),
directive.arguments,
astDirective.arguments,
variables)

directiveArgs match {
case Success(directiveArgs) =>
deprecatedArgsUsed(directive.arguments, directiveArgs).foreach { arg =>
deprecationTracker.deprecatedDirectiveArgUsed(directive, arg, ctx)
}
case _ => // if we fail to get args, the query should fail elsewhere
}

// nested argument directives
directive.arguments.foreach { nestedArg =>
nestedArg.astDirectives.foreach(trackDeprecatedDirectiveArgs)
}
case _ => // do nothing
}
}

def trackDeprecatedInputObjectFields(inputType: InputType[_], ioArg: Any): Unit =
inputType match {
case ioDef: InputObjectType[_] =>
ioDef.fields.foreach { field =>
// field deprecation
val fieldVal: Option[_] = (ioArg match {
case lm: ListMap[String, _] => lm.get(field.name)
case _ => None
}) match {
case Some(Some(nested)) => Some(nested)
case value => value
}

if (field.deprecationReason.isDefined && fieldVal.isDefined) {
deprecationTracker.deprecatedInputObjectFieldUsed(ioDef, field, ctx)
}

// for nested input objects
if (fieldVal.isDefined) trackDeprecatedInputObjectFields(field.fieldType, fieldVal.get)

// field directive args deprecation
field.astDirectives.foreach(trackDeprecatedDirectiveArgs)
}
case OptionInputType(ofType) =>
ioArg match {
case Some(ioArg) => trackDeprecatedInputObjectFields(ofType, ioArg)
case _ => trackDeprecatedInputObjectFields(ofType, ioArg)
}
case ListInputType(ofType) =>
ioArg match {
case seq: Seq[_] => seq.foreach(trackDeprecatedInputObjectFields(ofType, _))
case _ => // do nothing
}
case _ => // do nothing
}

val field = ctx.field
val astField = ctx.astFields.head

// field deprecation
val allFields =
ctx.parentType.getField(ctx.schema, astField.name).asInstanceOf[Vector[Field[Ctx, Any]]]
if (allFields.exists(_.deprecationReason.isDefined))
deprecationTracker.deprecatedFieldUsed(ctx)

// directive argument deprecation
field.astDirectives.foreach(trackDeprecatedDirectiveArgs)

// field argument deprecation
deprecatedArgsUsed(field.arguments, fieldArgs).foreach { arg =>
deprecationTracker.deprecatedFieldArgUsed(arg, ctx)
}

field.arguments.foreach { argDef =>
// argument directives args deprecation
argDef.astDirectives.foreach(trackDeprecatedDirectiveArgs)

// input object field deprecation
getArgValue(argDef.name, fieldArgs) match {
case Some(ioArg) => trackDeprecatedInputObjectFields(argDef.argumentType, ioArg)
case _ => // do nothing
}
}
}

private def resolveField(
userCtx: Ctx,
tpe: ObjectType[Ctx, _],
Expand Down Expand Up @@ -1572,8 +1693,7 @@ private[execution] class FutureResolver[Ctx](
deferredResolverState = deferredResolverState
)

if (allFields.exists(_.deprecationReason.isDefined))
deprecationTracker.deprecatedFieldUsed(ctx)
deprecationTracker.foreach(trackDeprecation(_, ctx))

try {
val mBefore = middleware.collect { case (mv, m: MiddlewareBeforeField[Ctx]) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ case class InputDocumentMaterializer[Vars](
schema,
variables,
document.sourceMapper,
DeprecationTracker.empty,
None,
(),
ExceptionHandler.empty,
None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ object QueryReducerExecutor {
operationName: Option[String] = None,
queryValidator: QueryValidator = QueryValidator.default,
exceptionHandler: ExceptionHandler = ExceptionHandler.empty,
deprecationTracker: DeprecationTracker = DeprecationTracker.empty,
deprecationTracker: Option[DeprecationTracker] = None,
middleware: List[Middleware[Ctx]] = Nil,
errorsLimit: Option[Int] = None
)(implicit executionContext: ExecutionContext): Future[(Ctx, TimeMeasurement)] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ private[execution] trait ResolverBuilder {
exceptionHandler: ExceptionHandler,
deferredResolver: DeferredResolver[Ctx],
sourceMapper: Option[SourceMapper],
deprecationTracker: DeprecationTracker,
deprecationTracker: Option[DeprecationTracker],
middleware: List[(Any, Middleware[Ctx])],
maxQueryDepth: Option[Int],
deferredResolverState: Any,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import scala.collection.immutable.VectorBuilder

class ValueCoercionHelper[Ctx](
sourceMapper: Option[SourceMapper] = None,
deprecationTracker: DeprecationTracker = DeprecationTracker.empty,
deprecationTracker: Option[DeprecationTracker] = None,
userContext: Option[Ctx] = None) {
import ValueCoercionHelper.defaultValueMapFn

Expand Down Expand Up @@ -527,7 +527,7 @@ class ValueCoercionHelper[Ctx](
isArgument))),
{ case (v, deprecated) =>
if (deprecated && userContext.isDefined)
deprecationTracker.deprecatedEnumValueUsed(enumT, v, userContext.get)
deprecationTracker.foreach(_.deprecatedEnumValueUsed(enumT, v, userContext.get))

val prepared = firstKindMarshaller match {
case raw: RawResultMarshaller => raw.rawScalarNode(v)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class ValueCollector[Ctx, Input](
schema: Schema[_, _],
inputVars: Input,
sourceMapper: Option[SourceMapper],
deprecationTracker: DeprecationTracker,
deprecationTracker: Option[DeprecationTracker],
userContext: Ctx,
exceptionHandler: ExceptionHandler,
fromScalarMiddleware: Option[(Any, InputType[_]) => Option[Either[Violation, Any]]],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ object BatchExecutor {
queryValidator: QueryValidator = QueryValidator.default,
deferredResolver: DeferredResolver[Ctx] = DeferredResolver.empty,
exceptionHandler: ExceptionHandler = ExceptionHandler.empty,
deprecationTracker: DeprecationTracker = DeprecationTracker.empty,
deprecationTracker: Option[DeprecationTracker] = None,
middleware: List[Middleware[Ctx]] = Nil,
maxQueryDepth: Option[Int] = None,
queryReducers: List[QueryReducer[Ctx, _]] = Nil,
Expand Down
Loading

0 comments on commit 6d14691

Please sign in to comment.